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
ContentEncoding — pick a response encoding
Returns the best-matching offered encoding for the request’s
Accept-Encoding header. Two offers tied on q go to the earlier one;
no acceptable offer returns "" (so the caller can choose to send no
encoding rather than substituting identity).
Encoding tokens have no parameters, so this function is unaffected by the v0.30 parameter-honouring change.
chosen := negotiate.ContentEncoding(r, []string{"gzip", "deflate"})
if chosen != "" {
w.Header().Set("Content-Encoding", chosen)
}Full source: docs/examples/standalone/contentnegotiation/main.go
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.