HTTP 406 is Not Acceptable: the resource has no representation that the request’s proactive negotiation headers (Accept, Accept-Language, Accept-Encoding and the rest) allow, and the server is unwilling to send a default instead (RFC 9110 section 15.5.7). It is about what the server would send back, not about what the client sent.
The exchange
GET /api/orders/1017 HTTP/1.1
Accept: application/xml
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{"error": "not_acceptable", "available": ["application/json"]}
RFC 9110 says the body should list the representations that are available, so the client can pick one.
Send it, or ignore the header
RFC 9110 section 12.4.1 gives the server a choice when nothing matches: honor the header with a 406, or disregard it and send the default. Many servers disregard it, and a request with no Accept at all expresses no preference. Browsers rarely hit a 406: their Accept for a page ends in */*;q=0.8, which takes anything. The 406s you see come from API clients that ask for exactly one type.
406 or 415
Accept: application/xml, the server only writes JSON 406 Not Acceptable
Content-Type: application/xml, the server only reads JSON 415 Unsupported Media Type
Who sends it
- Spring MVC throws
HttpMediaTypeNotAcceptableException, answered as 406, when no message converter can write a type theAcceptheader allows. - Rails raises
ActionController::UnknownFormatfromrespond_towhen the requested format has no block, and renders it as 406. - ASP.NET Core ignores
Acceptand sends JSON by default; withReturnHttpNotAcceptable = trueit answers 406. - Express’s
res.format()answers 406 when no listed type matches, unless you give it adefault. - Apache’s mod_negotiation sends a 406 listing the variants when none fits, unless
ForceLanguagePriority Fallbackis set.
Gotchas
- Parse
Accept, don’t compare strings.application/json; charset=utf-8,application/*,*/*andq=weights all count. Use the framework’s negotiation, such asreq.accepts()in Express. - Send
Vary. When the response depends onAccept,Vary: Acceptstops a cache from handing JSON to a client that asked for HTML. - Caching. 406 isn’t heuristically cacheable.
app.get('/api/orders/:id', async (req, res) => { // Express
if (!req.accepts('application/json')) {
return res.status(406).json({ error: 'not_acceptable', available: ['application/json'] });
}
res.json(await orders.get(req.params.id));
});
@GetMapping(value = "/api/orders/{id}", produces = MediaType.APPLICATION_JSON_VALUE) // Spring: 406 for other Accept types
public Order order(@PathVariable long id) { return orders.get(id); }
Search 406 415 in the tool below to compare the two.