HTTP content negotiation is the part of a request where client preferences meet the representations a server can actually provide. The headers are easy to confuse: Accept describes what the client would like to receive, Content-Type labels what a message body actually is, and Vary tells caches which request headers affected the choice.
The three headers do different jobs
A client can send:
GET /reports/42 HTTP/1.1
Accept: application/json
The server may answer:
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Accept is a request header. It can list several media types and attach quality values such as q=0.8 to express relative preference. It does not guarantee that the server supports every option.
Content-Type describes the representation in the current message. On a response, it tells the client how to interpret the returned bytes. On a request with a body, it tells the server how the client encoded those bytes.
Vary: Accept is cache metadata. It says that two requests for the same URL can legitimately receive different cached responses when their Accept headers differ.
A practical server decision
A useful implementation follows a fixed sequence:
- list the media types the route can produce;
- parse the client’s
Acceptfield; - choose the best supported representation;
- send that representation with the matching
Content-Type; - include
Varyfor every request header that influenced the choice; - return an error when no acceptable representation exists.
MDN documents 406 Not Acceptable as the response used when server-driven negotiation cannot produce a representation acceptable to the client. Some APIs instead fall back to a documented default. Whichever policy you choose, make it stable and testable.
Quality values are priorities, not versions
A request might say:
Accept: text/html, application/json;q=0.9, */*;q=0.1
The client prefers HTML, accepts JSON with a slightly lower priority, and will tolerate another type as a last resort. The server still chooses only among representations it supports.
Quality values should not become an improvised API-versioning scheme. A media type can carry a vendor subtype or parameter when the API contract requires it, but deployment compatibility, deprecation and client capability still need explicit rules.
Content-Type is not Accept
One of the most common API mistakes is validating the wrong header. For a JSON POST:
Content-Type: application/jsonsays the request body is JSON.Accept: application/jsonasks for a JSON response.
A client may send JSON while asking for a CSV response. The two fields are independent.
If a route cannot decode the submitted request format, the relevant failure is generally 415 Unsupported Media Type. If it can decode the request but cannot return an acceptable response representation, 406 Not Acceptable describes the negotiation failure.
Vary protects shared caches
Without the right Vary field, a shared cache can store the first response for a URL and serve it to later clients even when their negotiation headers differ. A browser asking for HTML could receive a cached JSON document, or a compressed response could be reused for a client that did not advertise that encoding.
Include only headers that genuinely affect representation selection. An unnecessarily broad Vary value fragments the cache and reduces reuse. Vary: * effectively prevents ordinary cache reuse because every request is treated as distinct.
This is closely related to validators. A cache may keep multiple variants, and each variant can have its own entity tag. Our guide to ETag vs Last-Modified explains how validators support conditional requests after a representation has been selected.
Language and encoding follow the same pattern
HTTP can negotiate more than media type:
Accept-Languageexpresses language preferences;Accept-Encodingexpresses supported content codings such asgziporbr;Varyrecords the request fields that changed the selected response.
The same discipline applies: pick from supported options, label the response, and tell caches what influenced the choice. Do not treat a missing preference as proof of a user’s country or identity. Language detection is a convenience, not a reliable profile.
Client hints and privacy
Proactive negotiation can expose information about the client. A long, unusual combination of language, encoding and other preference headers can add identifying detail. MDN notes that server-driven negotiation has privacy and efficiency drawbacks.
For large representation choices, an explicit URL or user setting can be clearer. A report endpoint might use /reports/42.csv or a format query parameter rather than relying entirely on an elaborate Accept list. The important point is consistency: URLs, cache keys and documentation should agree.
Tests worth keeping
Test more than the happy path:
- no
Acceptheader; - one supported exact media type;
- several types with quality values;
- a wildcard;
- every offered type set to
q=0; - an unsupported request
Content-Type; - a response variant missing the corresponding
Varyfield; - repeated requests through the actual CDN or reverse proxy.
Run the tests through the same cache layer used in production. Unit tests for the negotiation function will not reveal a proxy configuration that ignores or rewrites Vary.
A compact implementation rule
Treat negotiation as a contract between four facts: what the route can produce, what the client prefers, what the server selected, and what the cache must distinguish. Accept, Content-Type and Vary each carry one part of that contract.
Sources and version note
This guide was checked on October 27, 2026 against MDN’s HTTP content negotiation guide and its linked header references. HTTP specifications and platform behavior evolve; verify current browser, framework and intermediary documentation for your deployment.





