How HTTP Works

Glossary Term

Content Negotiation

Content negotiation lets one URL serve different formats, languages or encodings based on Accept headers. See q-values, Vary, 406 and the caching traps.

Reviewed 2 min readintermediate2 sourcesMarkdown
On this page

TL;DR: Content negotiation is one URL, several representations. The client states preferences with Accept, Accept-Language and Accept-Encoding; the server picks one and must list the headers it used in Vary.

Content negotiation is the HTTP mechanism for choosing among multiple representations of the same resource. In the common server-driven form, the client sends preference headers and the server selects a format, language or encoding, then labels the choice with Content-Type, Content-Language or Content-Encoding.

An example

GET /reports/42 HTTP/1.1
Host: api.example.com
Accept: application/json, text/csv;q=0.5, */*;q=0.1
Accept-Language: de-CH, de;q=0.9, en;q=0.5
Accept-Encoding: br, gzip
HTTP/1.1 200 OK
Content-Type: application/json
Content-Language: de
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding

How the choice is made

  • Each header lists options with optional quality values from 0 to 1. No q means 1; q=0 means “never”.
  • More specific ranges beat less specific ones regardless of order. text/html;level=1 overrides text/html, which overrides text/*, which overrides */*.
  • Accept-Encoding is the one negotiation that every site does, and compression is applied after the representation is chosen.

Non-obvious facts

  • Vary is not optional. A CDN or browser that caches a negotiated response without Vary will serve the wrong variant to the next user. The reverse, Vary: User-Agent or Vary: Cookie, fragments the cache into near-useless pieces; normalize the header at the edge or negotiate on something narrower.
  • 406 is rarely what you want. RFC 9110 allows the server to send a default representation instead of a 406, and most do. Browsers send */* anyway, so a 406 mostly appears on API endpoints with a strict Accept.
  • A file extension or query parameter is often better for caches. /reports/42.csv is cacheable, linkable and easy to test with curl, while Accept negotiation is invisible in the URL.
  • Languages are a bad place for surprises. Redirecting by Accept-Language alone breaks sharing and crawling; many sites offer a language switcher and treat the header as a hint.
  • Request bodies have the mirror image. A client’s Content-Type that the server does not accept yields 415 Unsupported Media Type, not 406.

Go deeper

Frequently asked questions

What is content negotiation in HTTP?

It is the mechanism by which a client and server pick the best representation of a resource, such as JSON or HTML, English or German, gzip or Brotli, for the same URL.

What does q=0.8 mean in an Accept header?

It is a quality value between 0 and 1 that ranks a preference. Types without q default to 1, and q=0 means not acceptable.

When should a server return 406?

When it can only produce representations the client excluded. In practice most servers ignore an unsatisfiable Accept and send a default, because a 406 is rarely more useful to the client.

Why must negotiated responses send Vary?

Because caches key on the URL by default. Vary tells a cache which request headers influenced the response, so it does not serve the German page to an English reader.

Sources

  1. MDN Web Docs: Content negotiationdeveloper.mozilla.org
  2. RFC 9110: Content Negotiationrfc-editor.org

Keep going

Browse /search