HTTP Content Negotiation
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');
}