Content Negotiation
CONTENT NEGOTIATION IS THE HTTP MECHANISM BY WHICH CLIENTS AND SERVERS AGREE ON THE FORMAT, LANGUAGE, AND ENCODING OF A RESPONSE — ENABLING A SINGLE ENDPOINT TO SERVE JSON, XML, CSV, OR VERSIONED MEDIA TYPES WITHOUT SEPARATE URLS. IGNORING CONTENT NEGOTIATION FORCES VERSIONING THROUGH URLS OR QUERY PARAMETERS AND MAKES FORMAT DISCOVERY OPAQUE.
When to Use
- Designing an API endpoint that must serve multiple response formats (JSON, XML, CSV)
- Implementing media-type-based API versioning (
application/vnd.myapp.v2+json) - Diagnosing a
406 Not Acceptableerror from a client or proxy - Deciding between URL versioning (
/v2/users) and header versioning (Accept: application/vnd.api.v2+json) - Supporting internationalized responses where language selection matters
- Building a public API where clients may request compressed or alternative encodings
- Reviewing a PR that hardcodes
Content-Type: application/jsonwithout honoring theAcceptheader - Configuring a reverse proxy or CDN to vary caching by
Acceptheader
Instructions
Key Concepts
Accept Header (Client-Driven Negotiation) — The client advertises acceptable response media types in order of preference using quality factors (
q=). The server selects the best match and responds with the chosen type inContent-Type. If no acceptable type is available, the server returns406 Not Acceptable.GET /reports/q1-2024 Accept: text/csv;q=0.9, application/json;q=1.0, */*;q=0.1The server reads this as: JSON preferred (
q=1.0), CSV acceptable (q=0.9), anything else as last resort.Content-Type Header — Declares the media type of the request body (on POST/PUT/PATCH) or response body. The client sets it on requests with bodies; the server sets it on responses. Mismatch between declared and actual type causes parsing failures.
POST /events Content-Type: application/json { "type": "order.completed", "orderId": "ord_123" }Media Types and Vendor Types — Media types follow the pattern
type/subtype[+suffix][;parameter]. Vendor types (application/vnd.*) allow APIs to declare version-specific or format-specific contracts. For example,application/vnd.github.v3+jsonis GitHub's versioned JSON type. The+jsonsuffix tells generic parsers they can treat the body as JSON even without specific type knowledge.Quality Factors (q values) — Values from
0.0to1.0indicating relative preference. Default is1.0.q=0means "not acceptable." Used inAccept,Accept-Language,Accept-Encoding, andAccept-Charsetheaders. Servers must implement negotiation logic that respects q-value ordering.Accept: application/json;q=1.0, application/xml;q=0.8, text/plain;q=0.5Vary Header — Tells downstream caches (CDNs, proxies, browsers) which request headers were used in content negotiation. A response that varies by
Acceptmust includeVary: Accept. Without this, a CDN may serve a JSON response to a client requesting CSV if both requests hit the same cache key.HTTP/1.1 200 OK Content-Type: application/json Vary: Accept, Accept-LanguageAccept-Encoding and Compression — Clients declare supported compression algorithms; servers respond with compressed bodies and
Content-Encodingheaders.gzipandbr(Brotli) are the most common. Compression negotiation is separate from format negotiation.GET /large-dataset Accept-Encoding: br, gzip;q=0.8HTTP/1.1 200 OK Content-Encoding: br Content-Type: application/json
Worked Example
GitHub's API demonstrates media-type versioning through content negotiation. GitHub uses Accept headers both for version selection and for enabling preview features:
Request the default v3 JSON response:
GET /repos/octocat/hello-world
Authorization: Bearer ghp_...
Accept: application/vnd.github.v3+json
HTTP/1.1 200 OK
Content-Type: application/vnd.github.v3+json
Vary: Accept, Authorization
X-GitHub-Media-Type: github.v3; format=json
{
"id": 1296269,
"name": "hello-world",
"full_name": "octocat/hello-world",
...
}
Request raw file content (format negotiation, same endpoint):
GET /repos/octocat/hello-world/contents/README.md
Accept: application/vnd.github.raw+json
HTTP/1.1 200 OK
Content-Type: text/plain
Vary: Accept
# Hello World
...
Enable a preview feature via Accept header (GitHub Reaction preview):
GET /repos/octocat/hello-world/issues/1
Accept: application/vnd.github.squirrel-girl-preview+json
The same URL returns an augmented response with reactions field when the preview media type is requested. This is GitHub's mechanism for progressive feature rollout without URL proliferation.
406 Not Acceptable — requesting an unsupported type:
GET /repos/octocat/hello-world
Accept: application/x-yaml
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{ "message": "Must accept 'application/vnd.github.v3+json'" }
Anti-Patterns
Ignoring the Accept header and always returning JSON. A server that returns
Content-Type: application/jsonregardless of theAcceptheader breaks negotiation. If the client requestsAccept: application/xmland receives JSON, it either rejects the response or silently parses wrong data. Fix: check theAcceptheader, return the negotiated type, and return406 Not Acceptableif no acceptable type is available.URL-based format selection instead of content negotiation. Adding
/users.jsonand/users.xmlas separate endpoints duplicates routing, skips theVaryheader (breaking CDN cache correctness), and adds URL surface area. HTTP already provides the mechanism: useAcceptheaders and vary cache responses accordingly.Omitting the Vary header on negotiated responses. A CDN that caches a JSON response without seeing
Vary: Acceptwill serve that cached JSON to all subsequent requests for the same URL — including clients requesting CSV. TheVaryheader is mandatory whenever response content differs based on request headers.Media-type versioning without a default. If an API requires
Accept: application/vnd.myapp.v2+jsonbut provides no fallback for plainAccept: application/json, existing clients that omit the vendor type receive a406. Always define a default version for generic JSON requests, documented in the API contract.
Details
Media-Type Versioning vs. URL Versioning
| Approach | Example | Pros | Cons |
|---|---|---|---|
| URL versioning | /v2/users |
Simple, visible, bookmarkable | URL proliferation, breaking resources |
| Query param | /users?version=2 |
Simple | Caching issues, not RESTful |
| Accept header | Accept: application/vnd.api.v2+json |
Clean URLs, proper HTTP | Less visible, harder to test in browser |
| Custom header | Api-Version: 2 |
Simple | Non-standard, not cached by Vary |
Media-type versioning via Accept is the most RESTful but requires CDN and proxy configuration for correct Vary handling. Most public APIs (Stripe, GitHub, Twilio) choose URL versioning for its simplicity and developer experience.
Real-World Case Study: Twilio Content Negotiation
Twilio's REST API accepts both application/json and application/x-www-form-urlencoded on request bodies (via Content-Type) and returns JSON by default. When Twilio added support for CSV exports on call logs, they used content negotiation rather than a separate /export endpoint:
GET /2010-04-01/Accounts/{AccountSid}/Calls.json
Accept: text/csv
Returns a CSV download of the same resource. The Vary: Accept header ensures CDN caches do not mix JSON and CSV responses. This avoided a URL proliferation problem that had plagued the earlier /Calls.json vs /Calls.xml pattern (which duplicated the file-extension suffix hack).
Source
- MDN — Content Negotiation
- RFC 9110 — HTTP Semantics, Section 12
- RFC 6838 — Media Type Specifications and Registration Procedures
- MDN — Accept Header
- MDN — Vary Header
Process
- Identify which dimensions of content negotiation are needed: format (JSON/XML/CSV), version (vendor type), language, and encoding.
- Implement
Acceptheader parsing in the server: parse quality factors, find the best match against supported types, return406if no match. - Set
Content-Typein every response to the exact negotiated media type (including vendor type if applicable). - Add
Varyheaders listing all request headers used in negotiation (Accept,Accept-Language,Accept-Encoding). - Run
harness validateto confirm skill files are well-formed.
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-versioning-header, api-http-caching, api-openapi-design
Success Criteria
- The server parses the
Acceptheader and returns the best-match media type inContent-Type. 406 Not Acceptableis returned when no client-acceptable type is available.- Every response whose content varies by a negotiated header includes an accurate
Varyheader listing those headers. - Media-type versioning uses vendor types (
application/vnd.*+json) and documents a default for genericapplication/jsonrequests. Accept-Encodingis honored for compression, withContent-Encodingset in compressed responses.