Not Acceptable
ActiveHTTP 406 Not Acceptable indicates the server cannot produce a response matching the proactive content negotiation headers sent by the client. Defined in RFC 9110 §15.5.7. The resource exists but not in a representation the client will accept (format, language, encoding).
Description
The 406 Not Acceptable status code indicates that the target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request. The server is unable to produce a response matching the list of acceptable values defined in the request's Accept, Accept-Language, Accept-Encoding, and Accept-Charset headers.
This is fundamentally a content negotiation failure. The server HAS the resource — it's not a 404. But the server cannot serialize it into a representation the client is willing to receive. The distinction from 415 Unsupported Media Type is directional: 406 means 'I can't give you what you want' (server → client), while 415 means 'I can't read what you gave me' (client → server).
The server SHOULD generate a payload containing a list of available representation characteristics and corresponding resource identifiers from which the user or user agent can choose the most appropriate. This allows the client to discover what formats ARE available and adjust its Accept headers accordingly.
In practice, 406 is relatively rare in modern APIs because most ignore Accept headers entirely and always return JSON. However, it becomes critical in APIs that use Accept-header-based versioning (e.g., Accept: application/vnd.github.v3+json), multilingual content delivery systems, and hypermedia APIs that support multiple serialization formats (JSON-LD, HAL, Siren).
Some frameworks return 406 aggressively when the Accept header doesn't exactly match available content types, while others fall back to a default representation. The strict interpretation per RFC is to return 406, but pragmatically many servers choose to respond with a default format rather than reject the request entirely.
Examples
GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/xml
Authorization: Bearer token123HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{"error": "not_acceptable", "message": "Cannot produce a response matching your Accept header.", "requested": "application/xml", "available": ["application/json", "application/hal+json", "text/csv"]}GET /api/repositories HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.v5+json
Authorization: Bearer token456HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{"error": "not_acceptable", "message": "API version v5 is not available.", "requested_version": "v5", "available_versions": ["v1", "v2", "v3"], "latest": "v3", "docs": "https://api.example.com/docs/versioning"}GET /articles/tech-trends HTTP/1.1
Host: content.example.com
Accept-Language: ja-JP, ja;q=0.9HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Content-Language: en
{"error": "not_acceptable", "message": "The requested resource is not available in Japanese.", "requested_language": "ja-JP", "available_languages": ["en", "es", "fr", "de"], "fallback_url": "/articles/tech-trends?lang=en"}Edge Cases
- •Most REST APIs ignore Accept headers entirely and always return JSON — sending Accept: application/xml to these APIs often returns 200 with JSON rather than 406. Strict content negotiation is opt-in.
- •The wildcard Accept: */* matches everything — a client sending this should never receive 406. But Accept: application/xml with no wildcard fallback is a strict constraint.
- •API versioning via Accept headers (Accept: application/vnd.api.v3+json) can produce 406 when the requested version doesn't exist — even though the resource itself exists in other versions.
- •Accept-Encoding negotiation rarely produces 406 in practice because servers can always fall back to identity (no encoding). Returning 406 for encoding is technically valid but almost never done.
- •Some frameworks (Rails, Django REST Framework) return 406 when a request has an Accept header that doesn't match any configured renderer/serializer, even if a default could be used.
- •Browser requests typically include Accept: text/html, application/xhtml+xml, */* — the wildcard means browsers almost never trigger 406. API clients with strict Accept headers are the usual source.
- •Content negotiation with quality values (q-factors) adds complexity: Accept: application/xml;q=0.9, text/plain;q=0.1 — the server must evaluate ALL options before deciding 406 is appropriate.
When You'll See This
- →Client requests application/xml from a JSON-only API
- →API versioning failure — Accept: application/vnd.api.v5+json but only v1-v3 exist
- →Language negotiation — content exists only in English but client sends Accept-Language: zh-CN
- →Hypermedia format mismatch — client wants HAL+JSON but server only produces JSON-LD
- →Mobile app with outdated Accept header after API format migration
- →Webhook consumer that only accepts application/xml receiving from a JSON-only event source
- →Content delivery network serving pre-rendered static files that don't match the requested encoding
Implementation References
| Language | Constant |
|---|---|
| Go | http.StatusNotAcceptable |
| Rust | http::StatusCode::NOT_ACCEPTABLE |
| Python | http.HTTPStatus.NOT_ACCEPTABLE |
| Node.js | http.STATUS_CODES[406] |
| .NET | HttpStatusCode.NotAcceptable |
| Java | HttpURLConnection.HTTP_NOT_ACCEPTABLE |
| PHP | Response::HTTP_NOT_ACCEPTABLE (Symfony) |
| Ruby | :not_acceptable (Rack) |
History
Introduced in HTTP/1.1 (RFC 2068, 1997) as part of the content negotiation framework. Carried forward unchanged through RFC 2616 (1999) and refined in RFC 9110 (2022) which clarified the relationship between proactive negotiation headers and the server's obligation to list available representations in the response body.
Related Status Codes
Related Headers
FAQ
What is the difference between 406 Not Acceptable and 415 Unsupported Media Type?
The distinction is directional. 406 means the server cannot produce a response in the format the client wants to RECEIVE (governed by the client's Accept headers). 415 means the server cannot understand the format the client SENT (governed by the request's Content-Type). Think of it as: 406 = 'I can't speak your language' (output problem), 415 = 'I can't read your language' (input problem). A request can theoretically trigger both if it sends an unreadable body AND demands an unproducible response format.
Should my API return 406 or just default to JSON when the Accept header doesn't match?
It depends on your API contract. Strict content negotiation (returning 406) is correct per RFC 9110 and appropriate for APIs that genuinely serve multiple formats, use Accept-header versioning, or serve multilingual content. However, most modern REST APIs that only produce JSON choose to ignore the Accept header and always return JSON — this is pragmatic and avoids breaking clients that send incorrect or overly specific Accept headers. The key question: does your API actually support multiple representations? If not, strict 406 enforcement adds friction without value.
How should I handle Accept-header-based API versioning and 406 responses?
When using Accept headers for versioning (e.g., Accept: application/vnd.myapi.v3+json), return 406 when the requested version doesn't exist, and include the list of available versions in the response body. Always include a 'latest' field pointing to the current version and a documentation URL. Consider also supporting a fallback: if the client sends Accept: application/vnd.myapi.v3+json, application/json;q=0.1, you can serve the latest version as the fallback rather than returning 406. This gives clients a graceful degradation path during version transitions.