How content negotiation leads to a 406
With each request, a client can say what it is prepared to receive: Accept for media types such as application/json or text/html, Accept-Language for languages and Accept-Encoding for compression. The server picks the best match. RFC 9110 lets a server either ignore preferences it can't meet and send its default, or refuse with 406. Most sites send the default, so 406s are uncommon, and they usually point to a strict API or a filtering rule.
A 406 response should list the formats that are available, though many servers send only a short error message.
What causes a 406 error
- A strict
Acceptheader. Asking an endpoint that only returns JSON fortext/html, or the other way round. - A required vendor media type. Some APIs pick their version from
Accept, such asapplication/vnd.example.v2+json, and refuse anything else. - An
Accept-Languagethe site has nothing for, on a server set to refuse rather than fall back to a default. - A filtering rule set to answer 406. Some web hosts configure their request filtering (ModSecurity rules, for example) to return 406 when a request looks suspicious, so a 406 can be a block.
Did the proxy or the site send the 406?
The site or API. A forward proxy passes your headers through untouched, and inside an HTTPS tunnel it can't read them. Send the same request without the proxy: the same 406 means the proxy isn't involved.
If the response body looks like a hosting or firewall page rather than the API's usual error format, a filtering rule probably sent it.
How to fix a 406 error
- Check the API docs for the media type it serves, and send exactly that in
Accept. - If any format will do, send
Accept: */*, or remove the narrowAcceptheader your code adds. - Remove or relax
Accept-Languageif the site may not have your language. - If a filtering rule sent the 406 on your own site, its log names the rule that matched. On someone else's site, contact the owner.
Fix the Accept header: curl and Python
# What the server returns with no format preference
curl -i -H "Accept: */*" https://api.example.com/items
# Ask for the format the API documents
curl -i -H "Accept: application/json" https://api.example.com/itemsimport requests
url = "https://api.example.com/items"
for accept in ("application/json", "*/*"):
r = requests.get(url, headers={"Accept": accept}, timeout=30)
print(accept, r.status_code, r.headers.get("Content-Type"))406 errors when you use proxies
A proxy doesn't change your Accept headers, so a 406 through a proxy is about your request. Changing IPs won't affect it.
What does change with location is which country's site you see, but the language you get still depends on the Accept-Language header you send. If you want the local version of a page, send the local language too; the geo-targeting preview shows the main language of each country you can target. See geo-targeting for what each product can target.
406 Not Acceptable: frequently asked questions
What does a 406 error mean?
Not Acceptable: the server has no version of the resource that matches the formats or languages your request said it would accept.
How do I fix a 406 Not Acceptable error?
Send an Accept header with a format the server provides, such as application/json for most APIs, or */* if any format will do. Relax Accept-Language if the site may not have your language.
What is the difference between 406 and 415?
406 is about the response: the server can't send a format you accept. 415 Unsupported Media Type is about your request body: the server can't read the format you sent.
Why does a site return 406 to my script when the page works in my browser?
Your script probably sends different headers. Browsers send a broad Accept header; a script may send a narrow one, or trip a filtering rule that a browser doesn't. Compare the headers of the two requests.