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

HTTP Content Negotiation

API Design Intermediate
debt(d5/e3/b3/t5)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches' (d5). Missing Vary headers or incorrect 406 responses require HTTP-aware testing tools, API integration tests, or cache debugging to detect. Standard linters won't catch semantic HTTP header issues — you need tools like Postman, curl testing, or specialized API contract validators to verify correct content negotiation behavior.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). Fixing content negotiation issues typically involves adding Vary headers, implementing proper Accept header parsing with quality values, or returning 406 responses — changes localized to response handling middleware or controller logic, touching a few files but not requiring architectural changes.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). Content negotiation is primarily an API boundary concern affecting response formatting logic. Once implemented correctly in middleware or a base controller, it doesn't impose ongoing costs across the codebase — the tax is paid at the HTTP layer, not throughout business logic.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5). The misconception explicitly states developers wrongly assume separate URLs are needed per format. Additionally, ignoring quality values (q=) and not understanding Vary header caching implications are documented gotchas that most API developers eventually learn through experience or debugging mysterious cache behavior.

About DEBT scoring →

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 42
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
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 1 ping F 0 pings S 0 pings S 2 pings M 1 ping T 3 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 3 pings W 0 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Scrapy 6 Google 5 Ahrefs 3 Bing 3 SEMrush 3 Claude 2 Meta AI 2 Perplexity 1 Majestic 1 PetalBot 1
crawler 24 crawler_json 3


✓ schema.org compliant