Match Accept media types case-insensitively - #2847
Open
ericproulx wants to merge 1 commit into
Open
Conversation
Media types are case-insensitive (RFC 9110 section 8.3.1), but the registered
ones are spelled in lower case and matched literally, so a differently-cased
Accept header found nothing:
Accept: TEXT/PLAIN -> served application/json
Accept: APPLICATION/VND.TWITTER-V1+JSON -> api.version nil
Neither failed loudly. Content negotiation fell through to the default
format, and header versioning behaved as though no version had been asked
for, so the request was served by whichever version matched first -- the
client quietly got something other than what it asked for.
Three sites decided this, all comparing against lower-case registered types:
the formatter's Accept lookup, MediaType.best_quality_media_type, and the
vendor pattern in MediaType.parse / .match?. Down-case the incoming media
type at each. The vendor pattern stays lower-case, which is the case a
vendor and version are declared in and therefore compared in.
Grape already treats media types this way when deciding whether to escape an
error body (Middleware::Error#html_content_type?, from #2789).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
fix/case-insensitive-media-types
branch
from
August 1, 2026 12:07
f33d287 to
550dd62
Compare
Danger ReportNo issues found. |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Media types are case-insensitive (RFC 9110 §8.3.1), but the registered ones are spelled in lower case and matched literally, so a differently-cased
Acceptheader found nothing:Accept: TEXT/PLAINagainst atext/plaincontent typeapplication/json(the default format)text/plainAccept: APPLICATION/VND.TWITTER-V1+JSONon a header-versioned APIapi.versionnil"v1"Accept: application/VND.twitter-v1+jsonapi.versionnil"v1"Neither failed loudly. Content negotiation fell through to the default format, and header versioning behaved as though no version had been asked for — so the request was served by whichever version matched first, and the client quietly got something other than what it asked for.
Approach
Three sites decided this, all comparing against the lower-case registered types:
Middleware::Formatter#format_from_header— theRack::Utils.best_q_matchlookupUtil::MediaType.best_quality_media_type— the same lookup on the versioning pathUtil::MediaType.parse/.match?— the vendor patternDown-case the incoming media type at each. The vendor pattern itself stays lower-case: that is the case a vendor and version are declared in via the DSL, and therefore the case they are compared in.
Grape already treats media types this way when deciding whether to escape an error body —
Middleware::Error#html_content_type?, added by #2789, compares withcasecmp?.Backward compatibility
No UPGRADING entry. A differently-cased
Acceptpreviously matched nothing, so nothing could depend on the old outcome beyond receiving the default format. Lower-case headers — effectively all real traffic — are unaffected.Longstanding rather than a regression: identical in 3.3.4.
Test plan
media_type_spec.rb(vendor/version/format parsed from an upper-case header, and type/subtype down-cased) and 2 inapi_spec.rb(content negotiation and vendor version resolution end to end); verified all four fail without thelib/change.🤖 Generated with Claude Code