HTTP Content Negotiation Explained
Content negotiation lets a client and server agree on a response's format, language, or encoding using Accept headers, before any body is sent.
Content negotiation is the mechanism by which an HTTP client and server agree on the best representation of a resource to return — its format, language, or compression — without the client needing to know in advance exactly what the server can produce. The same URL can return JSON to one client and XML to another, or English text to one client and French to another, based entirely on headers sent with the request.
Why negotiate at all
A single resource, like /users/42, can often be represented in more than one valid way: as JSON or XML, compressed with gzip or not, in English or Spanish. Rather than baking format into the URL (/users/42.json versus /users/42.xml) — which works, but conflates a resource’s identity with its representation — HTTP defines request headers that let a client state its preferences, and response headers that let the server say what it actually chose.
This matters because the resource (a particular user’s data) is a distinct concept from any representation of it (the specific bytes returned for a given format and language). Content negotiation keeps the URL identifying the resource, while headers handle which representation gets served.
The main negotiation headers
Accept tells the server which media types the client can handle, optionally ranked by preference using a q (quality) value from 0 to 1:
Accept: application/json, application/xml;q=0.8, text/plain;q=0.5
This says: JSON is preferred, XML is acceptable but less so, plain text is a fallback.
Accept-Language works the same way for languages:
Accept-Language: en-US, en;q=0.9, fr;q=0.5
Accept-Encoding states which compression schemes the client can decode, such as gzip or br (Brotli) — see gzip vs Brotli for how those specific schemes compare. A server can then compress the response body accordingly, shrinking payload size without the client asking for compression explicitly by format.
Accept-Charset (largely obsolete today, since UTF-8 is close to universal) let a client specify acceptable character encodings.
How the server responds
Once the server picks a representation, it should say which one it chose, most importantly via the Content-Type header on the response — this tells the client how to actually parse the body it’s about to receive. A well-behaved negotiating server also sets the Vary header, listing which request headers influenced its choice:
Content-Type: application/json
Vary: Accept, Accept-Language
Vary matters for caching. A cache sitting between client and server — whether a browser cache, a CDN, or an intermediate proxy — needs to know that different values of Accept could produce different responses for the same URL. Without Vary: Accept on a negotiated endpoint, a cache might serve a JSON response to a client that requested XML, simply because both requests hit the same URL. This is one of the header-driven caching behaviors that sits alongside Cache-Control and ETag, covered in HTTP caching headers.
Two negotiation strategies
Server-driven negotiation is what the headers above describe: the client states preferences, and the server picks a representation and returns it directly, in one round trip. This is the common case and what most APIs mean by “content negotiation.”
Agent-driven negotiation instead has the server respond with a list of available representations (typically via a 300 Multiple Choices status, one of the HTTP status codes), leaving the client to make a second request for the specific one it wants. This is rarely used in practice — it costs an extra round trip — but it exists in the specification for cases where server-side selection logic can’t confidently pick the best match on its own.
Content negotiation in REST APIs
For JSON-only REST APIs, full content negotiation is often skipped entirely — if a server only ever returns JSON, there’s nothing to negotiate, and the Accept header becomes a formality. It matters more for APIs that genuinely support multiple representations: public APIs offering both JSON and XML for backward compatibility, APIs versioned via Accept header parameters (Accept: application/vnd.myapi.v2+json) instead of URL paths, or any endpoint serving localized content where Accept-Language drives which translation comes back.
Content negotiation is also distinct from CORS, even though both involve request and response headers around cross-origin requests — CORS governs whether a browser is allowed to let a script read a cross-origin response at all, while content negotiation governs which representation of the resource that response actually contains.
The takeaway
Content negotiation lets clients and servers agree on a response’s format, language, and encoding through Accept-family request headers and matching response headers like Content-Type and Vary, without tying a resource’s identity to any one representation. It’s most visible in APIs that genuinely serve multiple formats or languages from the same endpoint — and even where it’s invisible, the Vary header it relies on still matters for getting caching right.
Tagged
Keep reading
The Lycoris Team · · 4 min read HTTP Methods Explained: GET, POST, PUT, PATCH, DELETE
HTTP methods signal intent — read, create, replace, update, or remove — and that intent determines caching, idempotency, and safety guarantees.
Takina · · 4 min read What Is Long Polling? A Simple Real-Time Alternative
Long polling holds an HTTP request open until new data arrives, faking real-time updates without a persistent connection. How it works and when to use it.
Takina · · 4 min read HTTP Status Codes Explained: A Practical Guide
HTTP status codes are three-digit responses that tell a client what happened to its request. A practical tour of the codes that actually matter.