Validate media type names, and fix Accept-Patch/Post/Query syntax checking - #157
Conversation
…Post parse_media_type() only checked that a media type contained a "/", leaving everything else to each field's ABNF regex. Accept-Patch, Accept-Post and Accept-Query had syntax = False, so they got no character-level checking at all: "@@@/###", '"quoted"/thing', "text/" and "a/b/c" all passed silently. Give Accept-Patch and Accept-Post the syntax their specs define (RFC 5789 is 1#media-type; LDP is #media-range, so it keeps wildcards), and move the media-type checks proper into check_media_type(): * The shape check now requires exactly one "/" with a non-empty name on each side, rather than just a "/" somewhere. * allow_wildcard now means something. It only exempted a bare "*" before, so "*/*" and "type/*" were accepted everywhere -- including on Accept-Patch, which lists media types, not media ranges. It now gates "*" in either position, and "*/subtype" is rejected in all cases. * Type and subtype names are checked against restricted-name from RFC 6838, Section 4.2, added as httplint/syntax/rfc6838.py. HTTP's token production is more permissive than the registry's rules, so "~text/pl%in" was clean before and is now MEDIA_TYPE_BAD_NAME; names over 127 characters are MEDIA_TYPE_LONG_NAME. Both are WARN, since such a value is legal HTTP and recipients will accept it -- it just can't be registered with IANA. Names that aren't valid tokens are left to the field's own syntax check, so there's no double reporting. Accept-Query keeps syntax = False; the current draft defines it as a Structured Field, so the media-type ABNF would reject its valid forms. Also fix split_list_field(), which tested each match for truth before stripping it, so a whitespace-only element survived as "". That produced a spurious bad-syntax note for values with trailing whitespace after a comma, e.g. "Accept-Patch: text/plain, ". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Accept-Query was implemented as a comma-separated list of media types, but
it isn't one. RFC 10008, Section 3 defines it as a List Structured Field
whose members are Tokens or Strings, each naming a media range without
parameters; media type parameters are carried as Structured Field
parameters. Its own example, which the old code mis-parsed, is:
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
Rebase the field on StructuredField with sf_type = "list", and check each
member with check_media_type(). Members that are neither Tokens nor Strings
are reported with the new ACCEPT_QUERY_BAD_TYPE. Wildcards are allowed --
the RFC permits "*/*" and "xxxx/*" -- so ACCEPT_QUERY_BAD_SYNTAX now talks
about media ranges rather than media types.
This changes the field's parsed value from a list of (media type, params)
tuples to the Structured Field list that http_sf returns.
The QUERY method was published as RFC 10008 in June 2026, so the reference
moves from the datatracker copy of draft-ietf-httpbis-safe-method-w-body to
the RFC.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BAD_Q_VALUE's summary used str.format syntax, but Note summaries are
%-formatted, so it rendered literally as:
The q value on '{media_type}' is invalid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewVerified the spec claims against the sources and ran the checks on the branch ( The three load-bearing citations hold up:
One detail worth calling out because it's easy to get wrong: 1. Non-token names in Accept-Query escape every check
# Names that aren't valid HTTP tokens are already reported by the field's own
# syntax check; RFC 6838 only adds information for the ones that are.
names = [name for name in names if re.match(rf"^{rfc9110.token}$", name, RE_FLAGS)]That premise doesn't hold for Accept-Query, which now has This is the same silent-pass class the PR set out to close, and it lands in the worst spot: RFC 10008 Strings exist precisely to carry names that aren't valid Tokens, so a String member is where a bad name is most likely to appear. Fix could be a 2. Bare
|
check_media_type() dropped names that aren't valid HTTP tokens, on the
grounds that the field's own ABNF check reports them. That doesn't hold for
Accept-Query, which has syntax = False because it's a Structured Field, so
nothing reported them at all:
Accept-Query: "text/pl(in" -> no notes
Accept-Query: "text/pl in" -> no notes
RFC 10008 Strings exist precisely to carry names that aren't valid Tokens,
so that's where a bad name is most likely to turn up. Add a check_token
argument for callers with no ABNF backstop, which reports such names with
the field's own bad-syntax note, and set it in accept_query.
Also drop the bare "*" escape hatch. Neither RFC 9110's media-range, LDP's
nor RFC 10008's permits it; for Accept and Accept-Post the field's ABNF
masked it, but "Accept-Query: *" passed silently.
While here:
* Rename RESTRICTED_NAME_CHARS to RESTRICTED_NAME_UNBOUNDED. It's a whole
restricted-name, not a character class, and sat one line from
rfc6838.restricted_name_chars, which is one.
* Lowercase the media ranges accept_query stores, as the other media-type
fields do; RFC 10008 members are case-insensitive.
* Cover a String member with a non-token name, a bare "*", case
normalisation, and a bad name in the type half rather than the subtype.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reproduced both issues before fixing; addressed in 927fbc5. 1. Non-token names in Accept-Query — fixedConfirmed exactly as described: Took the second of the two suggested routes: Chose that over reporting it as 2. Bare
|
Started from a review of 92fe78c (Accept-Patch / Accept-Post / Accept-Query), which factored media-type parsing into
parse_media_type(). That function only checked that a value contained a/, leaving everything else to each field's ABNF regex — and the three new fields hadsyntax = False, so they got no character-level checking at all.@@@/###,"quoted"/thing,text/,a/b/cand*/*all passed silently.Verifying the ABNF against the specs turned up two further problems, which are the second and third commits.
Added:check media type names, and the syntax of Accept-Patch/Accept-PostAccept-Patch and Accept-Post now declare the syntax their specs actually define — RFC 5789 is
1#media-type, LDP §7.1.1 is#media-range, so Accept-Post keeps wildcards — andHttpListFieldruns the check.Media-type checking proper moves into
check_media_type():/with a non-empty name either side, instead of a/somewhere.allow_wildcardnow does something. It only exempted a bare*before, so*/*andtype/*were accepted everywhere, including on Accept-Patch, which lists media types rather than ranges. It now gates*in either position, and*/subtypeis rejected in all cases.restricted-namefrom RFC 6838 §4.2, added ashttplint/syntax/rfc6838.py. This was unchecked for every media-type field, not just the new ones: HTTP'stokenpermits%,',*,`,|,~and a non-alphanumeric first character, none of which RFC 6838 allows. SoContent-Type: ~text/pl%inwas clean before and is nowMEDIA_TYPE_BAD_NAME; names over 127 characters areMEDIA_TYPE_LONG_NAME.Both new notes are WARN, not BAD — such a value is legal HTTP and recipients will accept it, it just can't be registered with IANA. Names that aren't valid tokens are left to the field's own syntax check, so there's no double reporting of a single problem.
I did not implement RFC 6838's SHOULD-level 64-character limit. Registered types such as
application/vnd.openxmlformats-officedocument.presentationml.slideshowsit close enough to it that the note would fire on legitimate values.Also fixes
split_list_field(), which tested each match for truth before stripping it, so a whitespace-only element survived as""and produced a spurious bad-syntax note for e.g.Accept-Patch: text/plain,.Changed:parse Accept-Query as a Structured Field, per RFC 10008Accept-Query isn't a comma-separated media-type list. RFC 10008 §3 defines it as a List Structured Field of Tokens or Strings, each naming a media range without parameters; type parameters ride along as SF parameters. The RFC's own example was mis-parsed by the old code:
Rebased on
StructuredFieldwithsf_type = "list", checking each member withcheck_media_type(). Wildcards are allowed here (the RFC permits*/*andxxxx/*). Members that are neither Tokens nor Strings get the newACCEPT_QUERY_BAD_TYPE.Breaking: the field's parsed
valuegoes from a list of(media type, params)tuples to the Structured Field listhttp_sfreturns.The reference also moves from the datatracker copy of
draft-ietf-httpbis-safe-method-w-bodyto RFC 10008, published June 2026. (92fe78c's message cited RFC 9694 for this field; that's Guidelines for the Definition of New Top-Level Media Types — commit message only, the code pointed at the draft.)Fixed:substitute the media type into the Accept q value noteBAD_Q_VALUE's summary usedstr.formatsyntax where Note summaries are%-formatted, so it rendered literally asThe q value on '{media_type}' is invalid.Reviewer notes
make test,make lintandmake typecheckare clean.;as parameter syntax before the value reaches us, so a sender writingapplication/sql;charset=UTF-8produces the conformant structure by accident.make tidywants to reformatnote.py,status.pyandset_cookie.py, which this branch doesn't touch. Reverted to keep the diff clean; the repo has some pre-existing black drift.Written by Claude Code (Opus 5) at mnot's direction. mnot asked for an assessment of the media-type validation gap first, reviewed and approved the proposed approach before any code was written, then approved the two follow-up fixes and the RFC reference update. The analysis, code, and spec citations are Claude's; each spec claim was verified against the RFC or W3C text during the session rather than recalled.
🤖 Generated with Claude Code