HTTP Content Negotiation
debt(d5/e3/b3/t5)
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.
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.
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.
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.
TL;DR
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
Common Misconception
Why It Matters
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
// 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
}
// 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');
}