What Is Content Negotiation? β Definition and How It Works
Learn how Accept headers select a representation, why Vary matters for caching, and how negotiation affects collected data.
Content negotiation is the mechanism by which a client and server agree on which representation of a resource to return β format, language, encoding β using request headers rather than distinct URLs.
1. The Request Headers
| Header | Selects |
|---|---|
Accept |
Media type β HTML, JSON, XML |
Accept-Language |
Natural language |
Accept-Encoding |
Compression |
Accept-Charset |
Character encoding (largely obsolete) |
Each accepts a weighted list:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-GB,en;q=0.9,fr;q=0.5
The q values are preferences from 0 to 1. Absent, q=1 is assumed.
2. The Same URL, Different Answers
This is the property that matters most for collection: one URL can return different content depending on headers.
curl -H "Accept: text/html" https://api.example.com/items/42 # an HTML page
curl -H "Accept: application/json" https://api.example.com/items/42 # JSON
Where a site offers JSON for the same URL as its HTML page, requesting it is strictly better than parsing markup β no selectors, no layout coupling, typed values. It is worth probing for on any API-backed site.
3. Vary Tells Caches What Mattered
A server that negotiates must declare which headers affected the response:
Vary: Accept, Accept-Language, Accept-Encoding
Without it, a cache stores one representation and serves it to everyone β the classic bug where a user gets another user's language, or a JSON client receives cached HTML.
For a client with its own cache, Vary is the instruction on what belongs in the cache key. Ignoring it produces exactly the same bug locally.
4. Language Negotiation Changes Your Data
Accept-Language is the header most often overlooked, and it silently changes collected values: month names, decimal separators, thousands separators, currency formatting, date order, and the entire text of a page.
A collection run that does not pin Accept-Language can produce a dataset whose number formats vary by whatever the default was β which is the sort of inconsistency discovered much later, in a parser.
Pin it explicitly, and record it alongside the data.
5. Negotiation Is Not the Only Mechanism
Sites also select representations by:
- Distinct URLs β
/en/pageand/fr/page, or.jsonand.htmlsuffixes. - Query parameters β
?format=json. - IP geolocation β which is not negotiation at all, and cannot be overridden by headers.
- Cookies β a stored language preference beating the header.
The geolocation case is the one that defeats header configuration: if the site decides by egress IP, only the egress location changes the answer.
6. Practical Guidance
- Probe for JSON. Send
Accept: application/jsonagainst a page URL; when it works, it eliminates HTML parsing entirely. - Pin
Accept-Languagefor every run, and store the value with the results. - Keep
Accept-Encodinghonest β advertise only what you can decode, and match the browser you claim to be. - Respect
Varyin any cache key you build. - Record the negotiated response headers, so a later change of representation is visible rather than mysterious.
- Check for a cookie or IP mechanism when headers appear to have no effect.
7. Summary
Content negotiation lets one URL serve multiple representations, selected by Accept, Accept-Language, and Accept-Encoding, with Vary telling caches which headers mattered.
For collection it cuts both ways: requesting JSON where a site offers it removes HTML parsing altogether, while an unpinned Accept-Language silently varies number formats, dates, and text β and where a site decides by IP instead, no header will change the answer.