A 406 Not Acceptable means the server cannot produce a response that matches the format the client requested. The client sent an
Accept
header saying “I want the response in this format,” and the server said “I cannot serve it in that format.” The request itself is fine – the URL exists, authentication passed, the server is healthy – but the two sides cannot agree on a response format.
This is one of the less common HTTP errors, and when it does appear it is often caused by a misconfigured web application firewall, a browser extension modifying request headers, or an API client sending the wrong
Accept
header rather than a genuine content negotiation failure.
Quick reference#
| Symptom | Likely cause | Fix |
|---|---|---|
| 406 in a browser on a specific site | ModSecurity or WAF blocking the request based on
Accept
header | Check WAF audit log for the triggering rule |
| 406 on API calls | Wrong
Accept
header (requesting a format the API does not support) | Check the API docs for supported formats and fix the
Accept
header |
| 406 after installing a WordPress plugin | Plugin modifying response headers or conflicting with content negotiation | Deactivate plugins one by one to find the culprit |
| 406 on all requests from one browser | Browser extension injecting or modifying
Accept
headers | Disable extensions, test in incognito |
| 406 from Hulu or streaming service | DRM/geo-restriction returning 406 instead of 403 | Not a server config issue – the service is blocking your client or region |
| 406 intermittent | Caching layer serving a cached response with wrong
Content-Type
| Purge the cache or add
Vary: Accept
to cache configuration |
What content negotiation is#
HTTP has a built-in mechanism for clients and servers to agree on the format of a response. The client sends
Accept
headers describing what it can handle, and the server picks the best match from what it can produce.
The key headers involved:
Accept: text/html, application/json, */*
Accept-Language: en-US, en;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Charset: utf-8
-
Accept– the media types the client can process (HTML, JSON, XML, images, etc.) -
Accept-Language– preferred languages -
Accept-Encoding– compression formats the client supports -
Accept-Charset– character encodings the client supports
The
q
values are quality weights from 0 to 1.
en;q=0.9
means “I prefer English but it is not my first choice.” Omitting
q
defaults to 1.0 (highest priority).
When a server receives these headers, it compares them against the formats it can produce. If there is a match, it serves the response in that format and includes a
Content-Type
header telling the client what it got. If there is no match and the server is strict about content negotiation, it returns 406.
In practice, most web servers and frameworks are lenient. A browser sends
Accept: text/html
and the server returns HTML. An API client sends
Accept: application/json
and the server returns JSON. The 406 error only appears when something forces the server into strict negotiation mode or when the
Accept
header is genuinely incompatible.
What the HTTP specification says#
HTTP 406 is defined in RFC 9110, Section 15.5.7. The specification says the server cannot produce a representation for the target resource that would be acceptable to the client, based on the proactive content negotiation header fields in the request.
The server should generate a response body containing a list of available representations and their characteristics, so the client can choose. In practice, most servers return a generic error page or a JSON error body.
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{
"error": "Not Acceptable",
"message": "Supported formats: application/json, application/xml",
"status": 406
}
The specification notes that a server may return the response in a default format rather than returning 406, even if the client did not explicitly request that format. This is why 406 errors are rare in normal browsing – most servers just serve HTML regardless of the
Accept
header.
Common causes#
ModSecurity or WAF blocking based on Accept header
The most common cause of 406 errors on production websites. ModSecurity and other web application firewalls inspect every request header, including
Accept
. Certain rules flag unusual or missing
Accept
headers as suspicious:
- A request with no
Acceptheader at all - An
Acceptheader with unusual media types - An
Acceptheader that matches a known attack pattern - A request where the
Acceptheader does not match the URL pattern (requesting JSON from an HTML endpoint)
ModSecurity often returns 406 instead of 403 Forbidden for these blocks because the WAF is technically rejecting the request based on content negotiation headers. Check the ModSecurity audit log:
tail -50 /var/log/modsec_audit.log
Look for the rule ID that triggered the block. If it is a false positive, disable the specific rule:
SecRuleRemoveById 920420
Rule 920420 in the OWASP Core Rule Set is a common trigger – it flags requests with a restricted
Accept
header.
Wrong Accept header in API calls
API clients that send the wrong
Accept
header get 406 from strict APIs:
# This fails if the API only supports JSON
curl -H "Accept: application/xml" https://api.example.com/users
# This works
curl -H "Accept: application/json" https://api.example.com/users
Common mistakes:
- Sending
Accept: text/htmlto a JSON-only API - Sending
Accept: application/xmlto a service that only serves JSON - Sending a malformed
Acceptheader (Accept: jsoninstead ofAccept: application/json) - Copying a header from documentation that uses a vendor-specific media type (
Accept: application/vnd.api+json) when the API also accepts standardapplication/json
The fix: check the API documentation for supported media types. Most REST APIs accept
application/json
. If you are unsure, use
Accept: <em>/</em>
to tell the server you will accept any format.
WordPress plugin conflicts
WordPress plugins that modify HTTP response headers can trigger 406 errors. Caching plugins, security plugins, and REST API plugins sometimes set headers that conflict with content negotiation:
- A caching plugin that strips or modifies
Acceptheaders on cached responses - A security plugin that adds strict content-type enforcement
- A REST API plugin that returns 406 for requests that do not match its expected media types
Diagnose by deactivating plugins one at a time. If the 406 disappears after deactivating a specific plugin, that plugin is the cause. Check the plugin’s settings for content-type or header-related options before removing it entirely.
Browser extensions modifying headers
Extensions that modify request headers can cause 406 errors. Privacy extensions, ad blockers, and developer tools may:
- Remove the
Acceptheader entirely - Replace the
Acceptheader with a non-standard value - Add conflicting
Acceptheaders
Test by opening the site in an incognito window with all extensions disabled. If it works in incognito, an extension is modifying the headers. Check the Network tab in DevTools to see the actual request headers being sent.
Strict content negotiation in API frameworks
Some API frameworks enforce strict content negotiation by default. If the framework is configured to return only JSON and the client requests XML, the framework returns 406 rather than falling back to a default format:
Django REST Framework:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
]
}
With only
JSONRenderer
configured, a request with
Accept: text/html
returns 406.
Express.js with res.format():
res.format({
'application/json': () => res.json(data),
'text/html': () => res.render('page', data),
default: () => res.status(406).send('Not Acceptable')
});
The
default
handler explicitly returns 406 for unsupported formats.
Spring Boot:
@GetMapping(value = "/api/users", produces = "application/json")
The
produces
annotation restricts the endpoint to JSON. Requests with incompatible
Accept
headers receive 406.
Streaming services (Hulu, Netflix)
A frequently searched variant: “Hulu 406 error.” Streaming services return 406 when their DRM or content delivery system rejects the client. This is not a content negotiation issue in the traditional sense – the service uses 406 to indicate the client is not acceptable for content delivery, which could mean:
- The geographic region is blocked
- The device or browser is not supported for DRM playback
- An ad blocker or VPN is interfering with the content delivery pipeline
This is a client-side restriction, not a server configuration issue. Try disabling VPN, ad blockers, and browser extensions. If using a browser, try a different one. If the issue persists, contact the service’s support.
How to diagnose#
Step 1 – Check the request headers
Open browser DevTools (Network tab) and find the failed request. Look at the request headers, specifically
Accept
:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
A normal browser sends a broad
Accept
header that includes
<em>/</em>
as a fallback. If the
Accept
header is narrow or unusual, something is modifying it.
Step 2 – Reproduce with curl
Test with curl to isolate the issue from browser extensions:
# Send with a standard Accept header
curl -v -H "Accept: text/html,application/xhtml+xml,*/*" https://example.com/page
# Send with no Accept header (curl's default is */*)
curl -v https://example.com/page
# Send with a specific Accept header to test strict negotiation
curl -v -H "Accept: application/json" https://example.com/page
If curl works but the browser does not, the issue is browser-specific (extension, cached headers, or modified request).
Step 3 – Check the server error log
# Nginx
tail -20 /var/log/nginx/error.log
# Apache with ModSecurity
tail -50 /var/log/modsec_audit.log
# Application logs
tail -50 /var/log/app/error.log
Look for 406-specific entries or WAF rule triggers.
Step 4 – Test in incognito
An incognito window disables most extensions and starts with default headers. If the 406 disappears in incognito, the cause is a browser extension.
Step 5 – Check if it is cached
If the 406 is intermittent, a caching layer might be serving a cached error response. Purge the relevant cache and test again:
# Test with cache bypass
curl -v -H "Cache-Control: no-cache" https://example.com/page
Fixing a 406#
Fix WAF false positives
If ModSecurity or another WAF is returning 406:
- Find the rule ID in the audit log
- Verify it is a false positive (the request is legitimate)
- Disable the specific rule or create an exception for the affected path
# Disable a specific rule
SecRuleRemoveById 920420
# Or create a path-specific exception
<Location /api/>
SecRuleRemoveById 920420
</Location>
Fix the Accept header
For API clients:
# Python requests
response = requests.get('https://api.example.com/data',
headers={'Accept': 'application/json'})
# JavaScript fetch
fetch('https://api.example.com/data', {
headers: { 'Accept': 'application/json' }
})
# curl
curl -H "Accept: application/json" https://api.example.com/data
If you do not know what format the API supports, use
Accept: <em>/</em>
and check the
Content-Type
header in the response to see what format the server returned.
Fix server-side content negotiation
If your application returns 406 when it should not, add a fallback format:
Django REST Framework:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]
}
Express.js:
res.format({
'application/json': () => res.json(data),
default: () => res.json(data) // fallback to JSON instead of 406
});
Fix caching issues
If a cache is serving 406 responses, add the
Vary
header to tell caches that responses vary by
Accept
header:
# Nginx
add_header Vary Accept;
# Apache
Header append Vary Accept
This ensures the cache stores separate responses for different
Accept
header values instead of serving an HTML-cached response to a JSON request.
406 vs related errors#
| Status code | What happened |
|---|---|
| 400 Bad Request | The request is malformed – bad syntax or encoding |
| 403 Forbidden | Access denied – you do not have permission |
| 406 Not Acceptable | The server cannot produce a response in the format you requested |
| 415 Unsupported Media Type | The server cannot process the format you sent (request body) |
| 422 Unprocessable Entity | The request is valid but the data fails validation |
The key distinction between 406 and 415: a 406 is about the response format (what the server sends back). A 415 is about the request format (what the client sends in the body). If the client sends JSON but the server only accepts XML, that is 415. If the client wants XML back but the server only produces JSON, that is 406.
Summary#
A 406 Not Acceptable means the server and client cannot agree on a response format. In production, this is most often caused by a WAF rule blocking the request based on the
Accept
header, not by a genuine content negotiation failure. Check the WAF audit log first. For API calls, verify the
Accept
header matches the API’s supported formats. For browser issues, test in incognito to rule out extensions. For streaming service 406 errors, disable VPN and ad blockers.