Content negotiation
server-middleware/negotiate
sits on top of mediatype and exposes two
single-purpose helpers — one for Accept, one for Accept-Encoding.
ContentType — pick a response media type
Returns the offer most acceptable to the request’s Accept header. If
two offers match with equal weight, the more specific offer wins
(text/* trumps */*; type/subtype trumps type/*); after that the
earlier entry in offers wins. If no offer is acceptable,
defaultOffer is returned.
// Pet is the demo resource served by the negotiation handler.
type Pet struct {
XMLName xml.Name `json:"-" xml:"pet"`
Name string `json:"name" xml:"name"`
}
func pickContentType() {
pet := Pet{Name: "Lassie"}
offers := []string{mediaTypeJSON, mediaTypeXML}
http.HandleFunc("/pet", func(w http.ResponseWriter, r *http.Request) {
chosen := negotiate.ContentType(r, offers, mediaTypeJSON)
w.Header().Set("Content-Type", chosen)
switch chosen {
case mediaTypeXML:
_ = xml.NewEncoder(w).Encode(pet)
default:
_ = json.NewEncoder(w).Encode(pet)
}
})
srv := &http.Server{
Addr: ":8080",
ReadHeaderTimeout: readHeaderTimeout,
}
log.Fatal(srv.ListenAndServe())
}Full source: docs/examples/standalone/contentnegotiation/main.go
When Accept is absent entirely, the first offer is returned
unchanged.
Behaviour change in v0.30 — MIME parameters honoured
Pre-v0.30 the negotiator stripped MIME-type parameters before matching:
an Accept of text/plain;charset=utf-8 matched an offer of
text/plain;charset=ascii (the charset was thrown away). That was
expedient but wrong; v0.30 honours parameters by default:
Accept: text/plain;charset=utf-8matches an offer of baretext/plain(offer carries no constraint — receiver-side params, asymmetric rule).Accept: text/plain;charset=utf-8does not match an offer oftext/plain;charset=ascii(charset values disagree).
If your producers and Accept clients use mismatched charset or
version params that you treat as informational, opt out per call —
chosen := negotiate.ContentType(r, offers, "",
negotiate.WithIgnoreParameters(true),
)Full source: docs/examples/standalone/contentnegotiation/main.go
— or server-wide via the runtime’s middleware.Context:
ctx := middleware.NewContext(spec, api, nil).SetIgnoreParameters(true)Full source: docs/examples/standalone/contentnegotiation/main.go
Accept-Encoding — not handled here
negotiate.ContentEncoding is deprecated. The runtime does not ship
response compression, and surfacing a half-feature negotiator without a
matching encoder leads to subtle correctness traps (no Vary,
no Content-Length rewrite, no minimum-size guard). Use a real
compression middleware at the http.Handler level — see the
compression recipe for a worked
example using
CAFxX/httpcompression.
Direct header parsing
If you only need raw header parsing without the typed MediaType
layer (for example when implementing a different selection rule), drop
down to
negotiate/header:
specs := header.ParseAccept(r.Header, "Accept")
for _, s := range specs {
// s.Value, s.Q, s.Params
use(s)
}Full source: docs/examples/standalone/contentnegotiation/main.go
Where it sits in the runtime pipeline
The full server pipeline calls ContentType (and the matching
Content-Type validation through mediatype.MatchFirst) inside
Context.BindValidRequest; see
core / interfaces.
The standalone module exposes the same primitives so you can drive
negotiation from any net/http handler, with or without an OpenAPI
spec in the picture.