Content negotiation — Accept header, multiple representations
Intermediate5 min read·eng-13-015
Concept
Content negotiation — the HTTP mechanism where client and server agree on the best representation of a resource. The client says what it can handle; the server picks the best match.
Why it exists: The same resource (/users/42) can be represented in multiple formats: JSON, XML, HTML, CSV. Content negotiation lets one endpoint serve different formats without different URLs.
Request headers for content negotiation:
Accept: Preferred response format.Accept: application/json, text/html;q=0.9.q=0.9is a quality/preference value (0.0–1.0, default 1.0). Higherq= more preferred.Accept-Language: Preferred language.Accept-Language: en-US, fr;q=0.8.Accept-Encoding: Preferred compression.Accept-Encoding: gzip, deflate, br. Server compresses response if it supports the encoding.Accept-Charset: Preferred character encoding. Rarely used today — UTF-8 is universal.
Response headers:
Content-Type: The actual format of the response.Content-Language: The language of the response.Content-Encoding: The compression used (e.g.,gzip). Client must decompress.Vary: Tells caches which request headers the response depends on.Vary: Accept-Encodingmeans the cache should store separate versions for gzip vs non-gzip.
Server-driven vs client-driven:
- Server-driven: Server picks the format (most common). Server reads
Acceptand chooses. - Client-driven: Server returns a list of options (300 Multiple Choices); client picks one. Rarely used.
Practical use in APIs:
- Most REST APIs only support JSON. Content negotiation is rarely implemented.
- But
Accept-Encoding: gzipIS used — Nginx gzips responses automatically. - APIs that serve both browsers and clients might check
Accept: text/htmlvsapplication/json.
Code Example
php
<?php
// ACCEPT HEADER — client states format preference
// Request: Accept: application/json, application/xml;q=0.8, text/html;q=0.5
// Means: prefer JSON, then XML, then HTML
// Reading Accept in Laravel
Route::get('/users/{id}', function (int $id, Request $request) {
$user = User::findOrFail($id);
// Check what the client accepts
if ($request->wantsJson() || $request->accepts('application/json')) {
return response()->json($user);
}
if ($request->accepts('text/html')) {
return view('users.show', compact('user'));
}
if ($request->accepts('text/csv')) {
return response()
->streamDownload(function () use ($user) {
echo "id,name,email\n{$user->id},{$user->name},{$user->email}";
}, 'user.csv', ['Content-Type' => 'text/csv']);
}
// Client requested a format we don't support
return response('Not Acceptable', 406); // 406 Not Acceptable
});
// respond() helper — auto-negotiation
Route::get('/reports', function (Request $request) {
$data = Report::all();
return $request->expectsJson()
? response()->json($data)
: view('reports.index', compact('data'));
});
// ACCEPT-ENCODING — compression (usually handled by Nginx/webserver)
// Client: Accept-Encoding: gzip, deflate, br
// Server: Content-Encoding: gzip
// Response body is compressed — client decompresses
// In PHP — gzip output manually (if not handled by Nginx)
Route::get('/large-data', function () {
$data = json_encode(User::all()->toArray());
$accepts = request()->header('Accept-Encoding', '');
if (str_contains($accepts, 'gzip')) {
return response(gzencode($data), 200, [
'Content-Encoding' => 'gzip',
'Content-Type' => 'application/json',
'Vary' => 'Accept-Encoding',
]);
}
return response($data, 200, ['Content-Type' => 'application/json']);
});
// VARY header — tells caches which request headers affect the response
Route::get('/users', function (Request $request) {
$format = $request->accepts('application/json') ? 'json' : 'html';
return response($format === 'json' ? json_encode([]) : '<html></html>')
->header('Content-Type', $format === 'json' ? 'application/json' : 'text/html')
->header('Vary', 'Accept'); // cache MUST store separately for json and html
});
// ACCEPT-LANGUAGE — localization
Route::get('/welcome', function (Request $request) {
$lang = $request->getPreferredLanguage(['en', 'fr', 'de']) ?? 'en';
App::setLocale($lang);
return response()->json(['message' => __('welcome')])->header('Content-Language', $lang);
});