HTTP 406 Not Acceptable

Not Acceptable

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 the Accept header allows.
  • Rails raises ActionController::UnknownFormat from respond_to when the requested format has no block, and renders it as 406.
  • ASP.NET Core ignores Accept and sends JSON by default; with ReturnHttpNotAcceptable = true it answers 406.
  • Express’s res.format() answers 406 when no listed type matches, unless you give it a default.
  • Apache’s mod_negotiation sends a 406 listing the variants when none fits, unless ForceLanguagePriority Fallback is set.

Gotchas

  • Parse Accept, don’t compare strings. application/json; charset=utf-8, application/*, */* and q= weights all count. Use the framework’s negotiation, such as req.accepts() in Express.
  • Send Vary. When the response depends on Accept, Vary: Accept stops 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.

Try it in the HTTP Status Codes

406 Not Acceptable

  1. 406Not Acceptable4xx Client error

    The server cannot produce a representation that the client’s Accept headers will take.

    When to use it

    An API that only speaks JSON receiving a request that insists on XML.

    Common mistakes

    Returning it because a header was absent, where the default representation would have been perfectly acceptable.

    Not cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.