The 406 Not Acceptable: Your Accept Header Named a Format the API Doesn't Return
Quick answer: A 406 Not Acceptable means the server cannot produce a response that matches the Accept header your client sent. It is not an auth problem and not a malformed body. The request was understood, but the content negotiation failed because the client asked for a representation the resource does not offer. Fix the Accept header, or remove it so the server can return its default format.
Content negotiation in one minute
HTTP content negotiation lets a client declare what media types it can handle. The Accept header carries a list of types, each with an optional quality value, and the server picks the best match. A 406 is the server's way of saying none of the offered types overlap with what it can return. This is easy to confuse with a 415 Unsupported Media Type, but the two are mirror images.
| Status | Header involved | Direction | Meaning |
|---|---|---|---|
| 406 Not Acceptable | Accept | What the client wants back | Server cannot return a matching type |
| 415 Unsupported Media Type | Content-Type | What the client is sending | Server cannot parse the request body |
A concrete reproduction
Imagine an endpoint that serves reports in JSON and PDF. A generated client, configured for an older service, sends a narrow header asking only for XML.
curl -i https://api.example.com/v1/reports/r-91 \
-H "Accept: application/xml"
If the report resource has no XML representation, the server returns 406 even though the report exists and the credentials are valid. The response often includes an Accept header listing the types actually available, which is the fastest clue you will get.
HTTP/1.1 406 Not Acceptable
Accept: application/json, application/pdf
Vary: Accept
Content-Type: application/problem+json
Why an AI agent can trigger this without noticing
Agents frequently copy a header from an unrelated example or hardcode a default Accept value when building a client. Because the header works fine against most endpoints, the bug only surfaces on a resource that enforces strict negotiation. Agents also tend to focus on the request body and the Content-Type header, treating Accept as boilerplate, so they do not check it when debugging. If you hand the agent a failing request, explicitly ask it to inspect every request header rather than only the body.
How to debug it methodically
- Re-run the request with
curl -vand read the outgoingAcceptheader. Confirm exactly what the client sent. - Remove the
Acceptheader entirely and retry. If the request now succeeds, negotiation was the problem. - Send a permissive header such as
Accept: */*and confirm a 200. This proves the resource is reachable. - Read the response
Acceptheader on the 406 and choose one of the listed types. - Pin the correct type in the client and add a regression test for that resource.
# Works: ask for a type the server actually offers
curl -i https://api.example.com/v1/reports/r-91 \
-H "Accept: application/json"
# Also works: let the server choose its default
curl -i https://api.example.com/v1/reports/r-91
Quality values and wildcard matching
An Accept header can express preferences rather than hard requirements. A value of application/json;q=0.9, */*;q=0.1 says "prefer JSON, but I will accept anything else." This is usually safer than a single rigid type because it gives the server a fallback. Reserve a strict, single-type header for cases where your client genuinely cannot parse anything else. If you build on top of an OpenAPI description, the responses section already tells you which media types the operation can produce, so the client should be generated from that list rather than guessed.
Fixing it at the client layer
For a JavaScript client, set the header per request or through a default that is permissive enough not to break strict endpoints.
const res = await fetch(`/v1/reports/${reportId}`, {
headers: {
Accept: 'application/json, */*;q=0.8',
},
});
if (!res.ok) throw new Error(`Unexpected status ${res.status}`);
return res.json();
Make sure the fix is not a global Accept: */* that papers over a resource that genuinely needs special handling, such as a binary download. The right header reflects what the calling code is prepared to parse.
The Vary header and caching
When responses differ based on the Accept header, the server should send Vary: Accept so caches keep the JSON and PDF representations separate instead of serving one client's format to another. If you debug a 406 that appears only intermittently, check whether a shared cache is serving a previously stored representation without re-running negotiation. Understanding the Vary header also explains why the same URL can return different content types legitimately; the resource is the same, but the representation is chosen per request.
Frequently asked questions
Can a 406 be caused by something other than the Accept header?
It is driven by content negotiation, which can also involve Accept-Charset, Accept-Encoding, or Accept-Language, but in modern APIs the Accept media type is the overwhelming cause. Start there.
Why does the same request work in the browser but fail in my client?
Browsers send a broad, permissive Accept header and let the server choose. A hand-written client often sends a narrow one copied from an example. Compare the two sets of outgoing headers with the network panel or curl -v.
Is returning 406 for a missing query parameter correct?
No. A bad or missing parameter is a 400 Bad Request. A 406 specifically means the server cannot satisfy the requested representation. If a server returns 406 for validation errors, that is a misclassification worth flagging to the API maintainer.
Should I just always send Accept: */*?
It avoids the error but loses the contract that helps the server and your code agree on the format. Prefer the specific type your code will parse, with a wildcard fallback at lower quality, and derive it from the OpenAPI responses when possible.
This is part of the The API Debugging Guide — start there for a systematic way to read status codes and request headers instead of guessing.