← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

HTTP Content Negotiation

api_design Intermediate

TL;DR

The HTTP mechanism by which clients declare what formats, languages, and encodings they accept (Accept, Accept-Language, Accept-Encoding) and servers respond with the best match — or 406 Not Acceptable if none fits.

Explanation

Content negotiation allows a single URL to serve multiple representations of the same resource. Clients send preference headers: Accept (MIME type), Accept-Language (human language), Accept-Encoding (compression), and Accept-Charset. Servers parse these using quality values (q=0.9) to select the best match and respond with Content-Type, Content-Language, and Content-Encoding headers confirming what was sent. Proactive (server-driven) negotiation is the most common: the server picks based on the Accept header. Reactive negotiation returns a 300 Multiple Choices response listing options. In PHP APIs, content negotiation enables one endpoint to serve JSON, XML, or MessagePack based on the client request, and to respond in the user's preferred language without separate URLs. The Vary header must be set to tell caches that responses differ by the negotiated dimension.

Watch Out

Omitting Vary: Accept on a negotiated response allows a CDN or shared cache to return a JSON response to a client that sent Accept: text/xml — cache poisoning via missing Vary is a subtle and hard-to-debug production bug.

Common Misconception

Content negotiation does not require separate URLs per format — the same endpoint serves JSON, XML, or HTML based on Accept headers, with Vary telling caches they are different representations.

Why It Matters

Proper content negotiation keeps APIs clean (no /resource.json vs /resource.xml URLs) and enables transparent internationalisation without query parameters — the language is a header concern, not a routing one.

Common Mistakes

  • Forgetting to set Vary: Accept — caches serve the first-seen format to all subsequent clients regardless of their Accept header.
  • Ignoring quality values (q=) in Accept headers — a client sending Accept: text/html, application/json;q=0.9 prefers HTML.
  • Returning 200 with a non-matching Content-Type instead of 406 — silent format mismatch causes parsing errors on the client.

Avoid When

  • Avoid content negotiation for format selection if your API has a single dominant consumer — a ?format=json query parameter is simpler and more debuggable.
  • Do not use content negotiation for API versioning — it couples version semantics to the Accept header and makes routing opaque.

When To Use

  • Use Accept-based negotiation when an API endpoint legitimately needs to serve multiple formats (JSON, XML, CSV) from the same URL.
  • Use Accept-Language negotiation for internationalised API responses where the language is request-scoped, not user-preference-scoped.
  • Always set the Vary header to match whichever Accept-* dimensions your responses vary on.

Code Examples

💡 Note
The bad example ignores the Accept header and omits Vary — an XML client silently receives JSON, and a cache serves that JSON response to all subsequent clients. The fix negotiates the format and sets Vary: Accept so caches store separate representations.
✗ Vulnerable
// Ignoring Accept header — always returns JSON, sets no Vary:
public function show(int $id): Response {
    $data = $this->repo->find($id);
    return response()->json($data); // XML client gets JSON, cache serves JSON to everyone
}
✓ Fixed
// Negotiate format, set Vary:
public function show(Request $req, int $id): Response {
    $data = $this->repo->find($id);
    $accept = $req->getAcceptableContentTypes();
    if (in_array('application/xml', $accept)) {
        return response()->xml($data)->header('Vary', 'Accept');
    }
    return response()->json($data)->header('Vary', 'Accept');
}

Added 31 Mar 2026
Views 14
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Google 2 Perplexity 1 Ahrefs 1
crawler 3 crawler_json 1

✓ schema.org compliant