Skip to content

openapi3: match media types case-insensitively - #1262

Draft
DmRomantsov wants to merge 2 commits into
getkin:masterfrom
DmRomantsov:fix/case-insensitive-media-types
Draft

DmRomantsov wants to merge 2 commits into
getkin:masterfrom
DmRomantsov:fix/case-insensitive-media-types

Conversation

@DmRomantsov

@DmRomantsov DmRomantsov commented Aug 31, 2026 •

Copy link
Copy Markdown

Motivation

A document that declares text/plain currently does not match a valid Content-Type: TEXT/PLAIN. openapi3.Content.Get returns nil, and request and response validation reject the body with an unexpected Content-Type error.

RFC 9110 section 8.3.1 defines media type and subtype tokens as case-insensitive.

Proposed changes

  • Preserve direct map lookups as the highest-precedence match, then compare only the media type and subtype case-insensitively at the existing full, stripped, and type/* matching stages.
  • Preserve parameter suffixes and their values instead of lowercasing the whole Content-Type value.
  • Resolve registered body decoders case-insensitively after checking for an exact registration, allowing matched request and response bodies to be decoded.
  • Resolve registered body encoders through the same lookup. A body matched under a case variant was decoded but could not be re-encoded, so setting a schema default failed after validation had already succeeded with rewriting failed: unsupported content type "APPLICATION/JSON". Decoder and encoder resolution now share one helper so they cannot drift apart again.
  • Match media types without allocating. The case-insensitive fallback sorted every declared media type on each miss, and a Content-Type carrying parameters reaches that fallback on every request. Candidates are now compared against the map directly, keeping the lexicographically smallest match so several declared case variants still resolve deterministically, and Get no longer re-searches the unparameterised mime that the previous stage already covered.
  • Add regression coverage for parameterized matches, exact-match precedence, deterministic resolution between declared case variants, wildcard fallback, malformed values, default rewriting, and request and response validation.
  • Add benchmarks for Content.Get, body decoder resolution, and whole-body validation, which the repository previously had none of.

Unrelated changes (optional)

None

Testing / Validation

  • Confirmed the new focused tests fail on master for Content.Get, request validation, and response validation before applying the implementation.
  • Confirmed the default-rewriting and encoder tests fail against the first commit alone, reproducing rewriting failed: unsupported content type "APPLICATION/JSON".
  • Checked the allocation-free matching against a copy of the previous implementation over every combination of 17 content maps and 25 mime values, requiring identical results.
  • make prepare
  • go test -count=1 ./...
  • go test -count=10 -short ./...
  • go test -count=2 -short -covermode=atomic ./...
  • Targeted race tests with -race -count=10
  • go vet ./...
  • Go modernize and nilness analyzers
  • goimports-reviser on changed Go files
  • shellcheck -S error maps.sh docs.sh

Benchmarks

benchstat over 10 runs, Apple M5 Pro, all deltas p=0.000. Both lookups are now allocation-free on every path.

Content.Get before after allocs
exact 5.19 ns 4.78 ns (−7.8%) 0 → 0
parameters 71.8 ns 51.9 ns (−27.7%) 1 → 0
case variant 75.9 ns 60.3 ns (−20.6%) 1 → 0
case variant + parameters 148.7 ns 110.1 ns (−25.9%) 2 → 0
no match 236.2 ns 117.6 ns (−50.2%) 3 → 0
parameters, 20 media types 408.4 ns 163.0 ns (−60.1%) 1 → 0
openapi3filter before after allocs
getBodyDecoder, exact 4.94 ns 4.88 ns 0 → 0
getBodyDecoder, case variant 383.6 ns 157.6 ns (−58.9%) 8 → 0
getBodyDecoder, unregistered (image/png) 373.3 ns 90.0 ns (−75.9%) 8 → 0
ValidateRequestBody, application/json 1.246 µs 1.225 µs (−1.7%) 37 → 37
ValidateRequestBody, ;charset=utf-8 1.298 µs 1.284 µs 38 → 37
ValidateRequestBody, APPLICATION/JSON 1.704 µs 1.458 µs (−14.4%) 46 → 37

image/png is the path every binary part of a multipart/form-data upload takes, since binary parts legitimately have no registered decoder.

Notes (optional)

Exact matches continue to win. Parameter values remain case-sensitive, type/* and */* retain their existing precedence, and media types missing a subtype continue to be rejected rather than falling through to a wildcard. No public API or dependency changes are introduced: the exported RegisteredBodyDecoder and RegisteredBodyEncoder keep their exact-match behaviour, and only validation resolves content types case-insensitively.

Parameter names are still compared case-sensitively, so a document declaring application/json;charset=utf-8 does not match application/json;Charset=utf-8 even though RFC 9110 section 5.6.6 makes parameter names case-insensitive. Handling that needs mime.ParseMediaType, which allocates a map per call, so it is left for a follow-up rather than added to this path.

Related history: #91, #93, and #1201.

Dependencies (optional)

None

DmytroRomantsovM and others added 2 commits August 31, 2026 11:54
RFC 9110 defines media type and subtype tokens as case-insensitive, so apply
case folding while preserving exact matches and parameter values.

Co-authored-by: Cursor <cursoragent@cursor.com>
Matching content types case-insensitively let a body declared as
text/plain be matched and decoded under a Content-Type of TEXT/PLAIN,
but re-encoding it still required an exact registration. Setting a
schema default on such a request therefore failed late, after
validation had already succeeded:

  rewriting failed: unsupported content type "APPLICATION/JSON"

decodeBody reports the media type using the casing from the header, and
encodeBody looked it up in bodyEncoders with an exact map access.

Resolve encoders through the same case-insensitive lookup already used
for decoders, extracted into a shared helper so the two cannot drift
apart again. The exported RegisteredBodyDecoder and RegisteredBodyEncoder
keep their exact-match behaviour, so no public API changes.

Also match media types without allocating: the case-insensitive fallback
sorted every declared media type on each miss, which a Content-Type
carrying parameters reaches on every request. Compare against the map
directly, keeping the smallest match so several declared case variants
still resolve deterministically, and skip re-searching the unparameterised
mime that the previous stage already covered.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants