You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(mediatype): expose RFC 6839 structured syntax suffix
MediaType now carries a Suffix field, populated by Parse from the
trailing '+'-delimited token of the subtype. A new Base() method
returns the base media type implied by the suffix (+json →
application/json, +xml → application/xml, +yaml → application/yaml)
or the receiver unchanged for unsuffixed or unknown-suffix types.
Subtype retains the full wire value, so existing callers comparing
Subtype against a string see no change. StripParams carries Suffix
through.
No matching, negotiation, or codec-lookup behavior changes in this
commit. This is the parsing primitive that later layers (alias
canonicalization, opt-in suffix-aware matching, codec-lookup
fallback for issue #140) build on.
Refs #140
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
* feat(mediatype): alias-aware matching with RFC 9512 alias table
Adds a fixed Aliases map seeded with the three YAML aliases that
RFC 9512 §2.1 explicitly enumerates as "Deprecated alias names for
this type" in the IANA registration template for application/yaml:
application/x-yaml, text/yaml, and text/x-yaml.
A new Canonical() method rewrites a MediaType through the table; a
new Match() returns a graded MatchKind {None, Alias, Exact} where
direct RFC 7231 agreement is MatchExact and alias-bridged agreement
is MatchAlias.
BestMatch and MatchFirst now consult the alias bridge as a
lower-priority fallback:
- BestMatch tie-break order becomes
(q, specificity, MatchKind, offer index). A request for
application/yaml against offers [application/x-yaml,
application/yaml] picks application/yaml regardless of offer
order; against [application/x-yaml] alone it picks the alias.
- MatchFirst runs a two-pass scan over the allowed list: pass 1
returns the first MatchExact, pass 2 returns the first
MatchAlias.
Direct callers of Matches are unchanged — strict RFC 7231 semantics
(no callers outside the package).
feat(mediatype): alias-aware codec lookup helper
Adds mediatype.Lookup[T] — a generic codec-map lookup with four
fallback tiers:
1. mediaType verbatim (fast path; preserves existing behavior for
callers that store and pass canonical strings).
2. Parsed canonical "type/subtype" form (strips params, lowercases).
Recovers the match when mediaType carries "; charset=...".
3. Alias-canonicalized form from Aliases — query side. Catches
"request asks for application/yaml, map keyed by canonical".
4. Walks the map and matches any key whose own alias-canonical
form agrees with the query — catches "map keyed under one
alias, query uses a different alias of the same canonical"
(e.g. registered under text/yaml, queried as application/x-yaml).
O(len(m)), negligible for codec maps.
The seam keeps alias definitions (and any future lookup tolerances)
in one place; both runtimes now route their codec-map lookups
through it.
Server-side response-producer lookups (context.go:559, 595, 612)
are intentionally untouched: they consume the negotiation output,
which is already alias-aware via the BestMatch / MatchFirst.
test(mediatype): pin param-aware Match behavior under alias bridge
Adds two rows to the Match table and one BestMatch scenario covering
the case where param disagreement defeats subtype agreement:
- alias subtype with matching params → MatchAlias
- exact subtype with param mismatch → MatchNone
- BestMatch picks the alias-with-matching-params offer over the
exact-subtype-with-mismatched-params offer, because the latter
fails the param-subset rule and isn't actually a match — not
because the alias tier outranks the exact tier on specificity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
(squash) refactor(mediatype): privatize alias and suffix-base maps
Renames the package-level Aliases and SuffixBase variables to their
lowercase counterparts. The maps were only ever exported to make
test introspection cheap; same-package tests reach them just as
well at lowercase, and Canonical / Base / Match / Lookup are the
intended external surface.
Privatizing removes:
- the mutation hazard from external code (these maps are read-only
by design but Go can't enforce that on an exported var);
- the implicit promise that the table shape is part of the API.
Refs #140
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
fix(runtime): YAMLMime defaults to the RFC 9512 canonical name
Flips the YAMLMime constant from "application/x-yaml" to
"application/yaml" (the IANA-registered canonical per RFC 9512).
The default client Consumers/Producers maps read YAMLMime as their
key and pick up the change automatically.
Backward compatibility is preserved end-to-end via the mediatype
alias bridge landed earlier.
- Requests / responses with Content-Type "application/x-yaml",
"text/yaml", or "text/x-yaml" still resolve to the YAML codec
(mediatype.Lookup tier 3-4, RFC 9512 §2.1 "Deprecated alias
names").
- Accept negotiation matches the legacy forms against canonical
offers and vice versa (Set.BestMatch / MatchFirst, MatchKind tier).
- User code that registered codecs explicitly at "application/x-yaml"
keeps working without changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
* feat(mediatype): opt-in RFC 6839 suffix tier for Match and Lookup
Refs #140
Introduces a single MatchOption mechanism (the AllowSuffix() opt-in)
that extends Match, BestMatch, MatchFirst, and Lookup to tolerate
RFC 6839 structured-syntax suffix media types (+json, +xml, +yaml)
when the caller explicitly asks for it. Default behavior is
unchanged.
- MatchKind gains MatchSuffix, slotted below MatchAlias. Order
is now None < Suffix < Alias < Exact, encoding semantic strength.
- MediaType.Match always returns the strongest tier that succeeds,
including MatchSuffix when the two values agree only after
folding the structured-syntax suffix (m.Base().Canonical().
Matches(other.Base().Canonical())).
- Set.BestMatch, MatchFirst, and Lookup accept variadic
...MatchOption arguments. With no options they reject
MatchSuffix candidates (same as today); with AllowSuffix() they
count them at lower priority than alias and exact matches.
- Lookup's tier 5 (opt-in) walks the map applying alias
canonicalization against the suffix base — query-side suffix
fold only, never a map-side suffix fold (the inverse case is
not a real scenario and would surprise users).
- MatchFirst becomes a 2- or 3-pass scan depending on opts:
exact → alias → suffix (if AllowSuffix). Tier ordering survives.
This is the core mechanism for loosening the contract negotiation contract. The user-
facing knobs (negotiate.WithMatchSuffix, middleware.Context.
SetMatchSuffix, client.Runtime.MatchSuffix) come in follow-up
commits.
feat(negotiate): WithMatchSuffix opt-in for RFC 6839 suffix tolerance
negotiate.WithMatchSuffix(true) extends ContentType to tolerate
structured-syntax suffix media types in Accept negotiation. When
enabled, an offer of application/vnd.api+json matches an Accept
entry of application/json and vice versa, for the suffixes the
runtime recognises (+json, +xml, +yaml).
Mirrors WithIgnoreParameters in shape: per-call Option, off by
default, plumbed to mediatype.Set.BestMatch as mediatype.AllowSuffix().
feat(middleware,client): SetMatchSuffix / Runtime.MatchSuffix opt-in
Wires the RFC 6839 suffix tolerance opt-in into the server and
client runtimes. The mediatype tier mechanics and the negotiate.
WithMatchSuffix Option landed in earlier commits on this branch
(ec89b9b, 386e79e); this commit threads the flag through both
runtimes so users can enable it server-wide or client-wide.
Server side (middleware/context.go, middleware/validation.go):
- Context grows a matchSuffix bool field.
- SetMatchSuffix(bool) *Context mirrors SetIgnoreParameters in
shape (fluent, server-wide).
- negotiateOpts() now emits WithMatchSuffix(true) in addition to
WithIgnoreParameters(true) when those flags are set.
- A new matchOpts() helper emits mediatype.AllowSuffix() for the
direct mediatype-level call sites.
- validateContentType signature grows an opts ...mediatype.MatchOption
parameter; both the validation-time call (validation.go) and
the codec-lookup call sites (context.go, validation.go) pass
matchOpts() through.
Client side (client/runtime.go):
- Runtime grows an exported MatchSuffix bool field.
- resolveConsumer, the post-pickConsumesMediaType gate, and the
inner producer check in pickConsumesMediaType all consult
r.matchOpts() (which emits mediatype.AllowSuffix() when set).
- pickConsumesMediaType's signature grows a trailing
opts ...mediatype.MatchOption parameter.
Default behavior unchanged — every existing test stays green. With
the flag on, a spec declaring consumes/produces in the base form
(e.g. application/json) end-to-end tolerates traffic that uses any
recognised structured-syntax variant (+json, +xml, +yaml).
Closes#140
docs(media-types): document alias bridge and WithMatchSuffix opt-in
Adds a "Beyond strict matching" section between the strict Matches
rule and the server inbound flow, covering:
- the MatchKind tier ordering (None < Suffix < Alias < Exact)
and how BestMatch / MatchFirst / Lookup rank candidates by it;
- the always-on alias bridge with the three RFC 9512 §2.1 YAML
aliases;
- the opt-in WithMatchSuffix / SetMatchSuffix / Runtime.MatchSuffix
surface, with the rationale (use it when you don't control both
sides of the wire, otherwise align the spec);
- tier interactions worth pinning: params still bind at every
tier, exact registrations always win, map-side suffix folding
is intentionally absent.
Plus a new gotcha entry on the +json / problem+json 415 case and
reference entries pointing at mediatype.Lookup and the three opt-in
surfaces.
Refs #140
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
---------
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/MEDIA_TYPES.md
+111Lines changed: 111 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -86,6 +86,105 @@ The asymmetry is intrinsic to the semantics ("loose if the bound has no
86
86
params, otherwise the constraint must be a subset"), not to which side is
87
87
the server.
88
88
89
+
## Beyond strict matching — alias and suffix tolerances
90
+
91
+
The bare `Matches` rule above is strict RFC 7231: type, subtype, and the
92
+
parameter subset. Two extensions sit on top of it, both surfaced through
93
+
the graded result of `MediaType.Match`:
94
+
95
+
| Tier | Reached when | Example |
96
+
|---|---|---|
97
+
|`MatchExact`| Strict RFC 7231 match. |`application/json` vs `application/json`|
98
+
|`MatchAlias`| Strict fails but both sides resolve to the same canonical form via the package-internal alias table. |`application/x-yaml` vs `application/yaml`|
99
+
|`MatchSuffix`| Strict and alias both fail but both sides resolve to the same base after folding the RFC 6839 structured-syntax suffix. |`application/vnd.api+json` vs `application/json`|
100
+
|`MatchNone`| None of the above. ||
101
+
102
+
`Set.BestMatch`, `MatchFirst`, and `mediatype.Lookup` rank candidates by
103
+
this tier in addition to q-value and specificity — when two offers fit a
104
+
constraint at different tiers, the stronger tier wins regardless of
105
+
offer order. Exact beats alias, alias beats suffix.
106
+
107
+
### Alias bridge — always on
108
+
109
+
RFC 9512 §2.1 enumerates three deprecated alias names for the
110
+
`application/yaml` registration:
111
+
112
+
| Alias | Canonical |
113
+
|---|---|
114
+
|`application/x-yaml`|`application/yaml`|
115
+
|`text/yaml`|`application/yaml`|
116
+
|`text/x-yaml`|`application/yaml`|
117
+
118
+
A request, offer, or codec registration in any of these forms matches a
119
+
counterpart in any of the others. The bridge is wire-format equivalence
120
+
backed by an explicit IANA registration-template field — no opt-in
121
+
needed and no way to disable it.
122
+
123
+
### Structured-syntax suffix tolerance — opt-in
124
+
125
+
`+json`, `+xml`, and `+yaml` are the RFC 6839 structured-syntax suffixes
126
+
the runtime recognises. Their wire format is the underlying base
127
+
(`+json` is JSON), but their semantics carry application-specific
128
+
structure on top (`application/problem+json` is JSON-on-the-wire with
129
+
the RFC 7807 problem-details document shape). Tolerating these as
130
+
equivalent to the base format is a contract loosening, so the runtime
131
+
defaults to strict and surfaces the leniency through an explicit
All three feed the same `mediatype.AllowSuffix()` option through
151
+
`Set.BestMatch`, `MatchFirst`, and `mediatype.Lookup`. With the flag on,
152
+
a spec declaring `consumes: [application/json]` end-to-end tolerates
153
+
request bodies sent with `Content-Type: application/vnd.api+json` (and
154
+
likewise for `+xml` / `+yaml`). With the flag off — the default — such
155
+
a request is rejected with 415, exactly as before.
156
+
157
+
The opt-in is intended for situations where the user does not control
158
+
both sides of the wire:
159
+
160
+
- a server that wants to accept `application/problem+json` errors from
161
+
upstream services declared as `application/json`;
162
+
- a client that needs to consume `application/problem+json` responses
163
+
from servers whose spec only declares `application/json` in `produces`.
164
+
165
+
If both sides are under your control, **prefer to align the spec**:
166
+
list `application/vnd.api+json` (or whichever variant applies)
167
+
explicitly in `consumes` / `produces`. The opt-in is leeway for the
168
+
common real-world mismatch, not a substitute for a faithful spec.
169
+
170
+
### Tier interactions worth pinning
171
+
172
+
-**Parameters still bind at every tier.** A constraint of
173
+
`application/yaml; charset=utf-8` does not match an offer of
174
+
`application/yaml; charset=ascii` even with subtypes equal — the
175
+
parameter-subset rule from `Matches` applies regardless of which tier
176
+
resolved the subtype. Suffix tolerance does not loosen the param
177
+
rule.
178
+
-**Exact registrations always win.** If `application/vnd.api+json` is
179
+
explicitly in `consumes` (or registered as a producer), routing and
180
+
codec lookup never fall through to the suffix tier for that mime —
181
+
even with `WithMatchSuffix(true)`.
182
+
-**Map-side suffix folding is intentionally absent.** A registration
183
+
at `application/vnd.api+json` does *not* receive a query of
184
+
`application/json` even with the opt-in. The inverse case ("only the
185
+
vendor consumer is registered, plain-base query arrives") is not a
186
+
scenario the runtime tries to cover.
187
+
89
188
## Server side — inbound `Content-Type` validation
90
189
91
190
Flow when a request arrives with a body:
@@ -395,6 +494,16 @@ your `produces` use mismatched charset/version params and you treat
395
494
those as informational, opt out with `negotiate.WithIgnoreParameters(true)`
396
495
(per call) or `Context.SetIgnoreParameters(true)` (server-wide).
397
496
497
+
**"My server rejects `application/vnd.api+json` (or `application/problem+json`) with 415."**
498
+
The default match is strict RFC 7231 — a vendor `+json` mime is *not*
499
+
a `application/json` mime. Two routes forward: (1) list the vendor
500
+
mime explicitly in the operation's `consumes` and register a codec
501
+
under that key (the spec-faithful path); or (2) enable
502
+
`Context.SetMatchSuffix(true)` server-wide to fold `+json` / `+xml` /
503
+
`+yaml` to the underlying base codec at lookup time (the leeway path,
504
+
for situations where the client is not under your control). See
505
+
the "Beyond strict matching" section above.
506
+
398
507
**"My client request returns 415 even though the API lists my type in `consumes`."**
399
508
Check the wire `Content-Type` against your server's `consumes` matching
400
509
rules. The client sends the picker's choice (with Stage-2 upgrades for
@@ -425,6 +534,8 @@ or the cached value at `middleware.MatchedRouteFrom(r).Consumes`.
425
534
426
535
- Server matching primitive: `github.com/go-openapi/runtime/server-middleware/mediatype`
427
536
- Server negotiator: `github.com/go-openapi/runtime/server-middleware/negotiate`
537
+
- Codec lookup helper: `mediatype.Lookup[T]` — used by both server (`middleware/context.go`, `middleware/validation.go`) and client (`client/runtime.go`)
538
+
- Alias and suffix tolerances: `mediatype.Match`, `mediatype.MatchKind`, `mediatype.AllowSuffix`; opt-in surfaces `negotiate.WithMatchSuffix`, `middleware.Context.SetMatchSuffix`, `client.Runtime.MatchSuffix`
428
539
- Server validation: `middleware/validation.go` (`validateContentType`)
0 commit comments