< HTTP/1.1 406 Not Acceptable406 Not Acceptable
The server cannot produce a response matching the client's Accept headers. Learn about content negotiation and how to handle format mismatches.
- Cacheable
- Only with explicit freshness
- Retry?
- No: relax the Accept headers
- Usually sent by
- Origin app
- Spec
- RFC 9110 §15.5.7
- Often confused with
- 415
On this page
- What you see in your client
- What is 406 Not Acceptable?
- Common causes by stack
- Fix a 406 on a SOAP call
- When Does This Happen?
- Example Responses
- Real-World Example
- 406 vs Other Client Error Codes
- Important Characteristics
- Common Mistakes
- Getting 406 Not Acceptable right
- Content Negotiation Preferences
- Implementation Examples
- Try It Yourself
- Try it with curl
- Related Status Codes
TL;DR: 406 Not Acceptable means the server cannot return a response in a format your request accepts. Check
Acceptfirst, then request a supported media type; for SOAP, match the response type to the SOAP version.
What you see in your client
These are the client formats for a response carrying 406 and the reason phrase Not Acceptable; the URLs are placeholders. They are derived from library source, rather than captured server output. A server-supplied reason phrase can change the Python, .NET and Spring text.
- Axios:
AxiosError: Request failed with status code 406. With the default status validation, the promise rejects; inspecterror.response.status,error.response.dataanderror.response.headers. Axios constructs this message insettle. - fetch: the promise resolves when the HTTP response arrives.
response.status === 406andresponse.ok === false; check those before reading a success payload. A network failure rejects separately. MDN documents this distinction. - Python requests:
requests.exceptions.HTTPError: 406 Client Error: Not Acceptable for url: https://api.example.com/resource. This comes fromresponse.raise_for_status(), not fromrequests.get()alone. Save the body before raising if it contains useful diagnostics. Requests source. - curl
-f:curl: (22) The requested URL returned error: 406.curl -ishows headers and the error body without fail mode;--fail-with-bodykeeps the body while returning a failing exit code. curl source, option documentation. - .NET:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 406 (Not Acceptable).This is the English message fromEnsureSuccessStatusCode(); inspect theHttpResponseMessagefirst when you need its body. Runtime message template. - Spring:
WebClientResponseException.NotAcceptablewith message406 Not Acceptable from GET https://api.example.com/resourceforWebClient.retrieve(). RestTemplate’s default error handler usesHttpClientErrorException.NotAcceptable; its message can include the response body. These class names follow the Spring 6.2 WebClient source and HTTP client source.
What is 406 Not Acceptable?
A 406 Not Acceptable status code means the server cannot produce a response that matches the client’s Accept headers. A JSON-only endpoint can reject Accept: application/pdf because it has no PDF representation.
This happens during content negotiation when the server understands the request but can’t deliver the content in the format, language, or encoding the client requested.
Common causes by stack
- Express:
res.format()finds no matchingAcceptmedia type and has nodefaultcallback. Add the missing representation or a deliberate fallback; inspectreq.accepts(['json', 'xml'])before selecting a format. Express response API. - Django REST Framework: none of the view’s renderers can satisfy
Accept, so DRF raisesNotAcceptablewith status 406. Checkrenderer_classesorDEFAULT_RENDERER_CLASSES; a JSON-only view needsAccept: application/json. DRF exceptions. - Spring Boot / Spring MVC:
HttpMediaTypeNotAcceptableExceptionmaps to 406. Compare the controller’sproducesmedia types with the request’sAccept, then check the configured message converters. An XML response needs an XML-capable converter. Spring resolver mappings. - ASP.NET Core MVC: with
ReturnHttpNotAcceptable = true, anObjectResultreturns 406 when no output formatter can satisfyAccept. RegisterAddXmlSerializerFormatters()when you intend to serve XML, or request JSON. Browser Accept headers are ignored by default unlessRespectBrowserAcceptHeaderis enabled. Microsoft formatting documentation. - Apache:
mod_negotiation, includingOptions MultiViews, cannot choose an acceptable file variant. Check the variant media types and language metadata; useOptions -MultiViewsif the route should resolve directly rather than negotiate filenames. Apache negotiation.
Fix a 406 on a SOAP call
Start with the outgoing HTTP headers, before debugging the XML envelope. SOAP 1.1 uses text/xml; SOAP 1.2 uses application/soap+xml. An endpoint returning SOAP XML cannot satisfy a JSON-only Accept preference under strict negotiation. Set Accept to the response media type documented for that binding. SOAP 1.1 HTTP binding, SOAP 1.2 media type.
For a SOAP 1.2 endpoint, this request keeps the input and output media types separate:
curl -i https://api.example.com/soap \
-H 'Content-Type: application/soap+xml; charset=utf-8' \
-H 'Accept: application/soap+xml' \
--data-binary @request.xml
A 406 points you toward the response preference; 415 points toward the request body type. Compare a failing SOAP client’s headers with a successful request to the same endpoint. Keep SOAPAction and SOAP 1.2’s action parameter aligned with the service binding; changing Accept cannot fix an operation-name mismatch.
When Does This Happen?
You’ll see a 406 Not Acceptable response in these common situations:
1. Unsupported Media Type
Client requests JSON, server only has XML
Accept: application/json → 406 (only text/xml available)
2. Unsupported Language
Client requests Spanish, server only has English
Accept-Language: es → 406 (only en available)
3. Unsupported Encoding
Accept-Encoding: gzip still allows an uncompressed response. Identity is excluded with identity;q=0, or *;q=0 without a more specific identity entry. RFC 9110 section 12.5.3.
Client excludes uncompressed responses, server only has identity
Accept-Encoding: gzip, identity;q=0 → 406 if server refuses the preference
4. Unsupported Charset
Client requests UTF-16, server only has UTF-8
Accept-Charset: utf-16 → 406 (only utf-8 available)
5. API Version Mismatch
Client requests v2 format, server deprecated it
Accept: application/vnd.api.v2+json → 406
Example Responses
The wire responses on this page are illustrative; available formats and error bodies belong to the application.
Media Type Mismatch:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Vary: Accept
{
"error": "Not Acceptable",
"message": "Cannot produce response in requested format",
"requested": "application/pdf",
"available": [
"application/json",
"text/html",
"application/xml"
]
}
Language Not Available:
HTTP/1.1 406 Not Acceptable
Content-Type: text/html; charset=utf-8
Content-Language: en
Vary: Accept-Language
<!DOCTYPE html>
<html lang="en">
<head><title>Language Not Available</title></head>
<body>
<h1>Requested Language Not Available</h1>
<p>The requested language (es) is not available.</p>
<p>Available languages: en, fr, de</p>
</body>
</html>
Encoding Not Supported:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Vary: Accept-Encoding
{
"error": "Encoding not acceptable",
"message": "Server cannot produce response with requested encoding",
"requested_encoding": "compress, identity;q=0, *;q=0",
"supported_encodings": ["gzip", "deflate", "br", "identity"]
}
Real-World Example
Imagine you’re building an API client that only accepts JSON responses:
Client Request (JSON Only):
GET /api/users/12345 HTTP/1.1
Host: api.example.com
Accept: application/json
Accept-Language: en-US
Accept-Encoding: gzip, deflate
Server Only Has XML:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Vary: Accept
Link: <https://api.example.com/docs/formats>; rel="help"
{
"status": 406,
"error": "Not Acceptable",
"message": "Cannot produce response in application/json format",
"details": {
"requested_format": "application/json",
"available_formats": [
{
"media_type": "application/xml",
"example_url": "/api/users/12345?format=xml"
},
{
"media_type": "text/html",
"example_url": "/api/users/12345.html"
}
]
},
"help_url": "https://api.example.com/docs/formats"
}
Alternative: Server Ignores Accept and Returns Default:
HTTP/1.1 200 OK
Content-Type: application/xml
Vary: Accept
<?xml version="1.0" encoding="UTF-8"?>
<user>
<id>12345</id>
<name>John Doe</name>
</user>
406 vs Other Client Error Codes
| Code | Meaning | Issue | Solution |
|---|---|---|---|
| 406 | Not acceptable | Response format unavailable | Request different format |
| 415 | Unsupported media type | Request format unacceptable | Send different format |
| 400 | Bad request | Malformed request | Fix request syntax |
| 404 | Not found | Resource doesn’t exist | Check URL |
Important Characteristics
Content Negotiation Failure:
Client: Accept: application/pdf
Server: Can only produce: application/json, text/html
Result: 406 Not Acceptable
Proactive vs Reactive:
Proactive negotiation:
- Client specifies preferences in Accept headers
- Server selects best match OR returns 406
Reactive negotiation:
- Server returns 300 Multiple Choices
- Client selects from available options
Vary Header Important:
List the request fields used to select the representation:
HTTP/1.1 406 Not Acceptable
Vary: Accept, Accept-Language
Content-Type: application/json
Common Mistakes
Include the available formats
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain
Not acceptable.
Use 415 for an unsupported request body
POST /api/users
Content-Type: text/csv ← Request body format issue
HTTP/1.1 406 Not Acceptable ← Wrong! Should be 415
Choose strict negotiation or a default deliberately
Accept: application/vnd.custom.v99+json
HTTP/1.1 406 Not Acceptable ← Valid strict negotiation; ignoring Accept is another policy
Return useful negotiation details
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Vary: Accept
{
"error": "Not Acceptable",
"available_formats": ["application/json", "text/html"],
"documentation": "/docs/api-formats"
}
Getting 406 Not Acceptable right
Provide List of Available Formats:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Vary: Accept
Link: <https://api.example.com/users/123?format=json>; rel="alternate"; type="application/json"
Link: <https://api.example.com/users/123?format=xml>; rel="alternate"; type="application/xml"
{
"error": "Format not available",
"requested": "application/pdf",
"available": [
{
"format": "application/json",
"url": "/users/123?format=json"
},
{
"format": "application/xml",
"url": "/users/123?format=xml"
},
{
"format": "text/html",
"url": "/users/123.html"
}
]
}
Consider Serving Default Format:
// Instead of strict 406, serve default if possible
app.get('/api/resource', (req, res) => {
const accept = req.accepts(['json', 'xml', 'html'])
if (!accept) {
// Could return 406, but serving default is more user-friendly
console.warn('No acceptable format, serving JSON')
return res.json({ data: resource })
}
switch (accept) {
case 'json':
return res.json({ data: resource })
case 'xml':
return res.type('xml').send(toXML(resource))
case 'html':
return res.render('resource', { data: resource })
}
})
Include Documentation Links:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
Link: <https://docs.example.com/api/content-negotiation>; rel="help"
{
"error": "Not Acceptable",
"message": "Requested format not available",
"documentation": "https://docs.example.com/api/content-negotiation"
}
Use Vary Header Correctly:
HTTP/1.1 406 Not Acceptable
Vary: Accept, Accept-Language, Accept-Encoding
Content-Type: application/json
{
"error": "Content negotiation failed",
"details": "Cannot provide es-MX in gzip encoding"
}
Content Negotiation Preferences
Quality Values:
Accept: application/json;q=1.0, application/xml;q=0.8, text/html;q=0.5
Wildcard Handling:
Accept: application/json, */*;q=0.1
Server Response Strategy:
parseAcceptHeader below is a project parser, not a built-in API. It must resolve wildcards against supported types, apply the most specific range and sort by effective quality. An absent Accept field accepts any media type; q=0 excludes a matching representation.
function selectFormat(acceptHeader) {
const supported = ['application/json', 'application/xml']
// Project helper resolves wildcards and specificity against supported types.
const preferred = parseAcceptHeader(acceptHeader, supported)
for (let format of preferred) {
if (format.q > 0 && supported.includes(format.type)) {
return format.type
}
}
throw new NotAcceptableError(supported)
}
Implementation Examples
Express.js:
app.get('/api/users/:id', (req, res) => {
const user = getUser(req.params.id)
// Content negotiation
res.format({
'application/json': () => {
res.json(user)
},
'application/xml': () => {
res.type('xml').send(userToXML(user))
},
'text/html': () => {
res.render('user', { user })
},
default: () => {
// 406 Not Acceptable
res.status(406).json({
error: 'Not Acceptable',
available: ['application/json', 'application/xml', 'text/html']
})
}
})
})
Django:
Django 5.2 added get_preferred_type(). Use it instead of substring matching, which mishandles q=0 and wildcards. Vary: Accept keeps negotiated responses separate in caches.
from django.http import JsonResponse, HttpResponse
from django.shortcuts import render
from django.views.decorators.vary import vary_on_headers
from django.utils.decorators import method_decorator
from django.views import View
class UserDetailView(View):
@method_decorator(vary_on_headers("Accept"))
def get(self, request, user_id):
user = get_user(user_id)
accepted = request.get_preferred_type([
'application/json', 'application/xml', 'text/html'
])
if accepted == 'application/json':
return JsonResponse({'user': user})
elif accepted == 'application/xml':
return HttpResponse(user_to_xml(user),
content_type='application/xml')
elif accepted == 'text/html':
return render(request, 'user.html', {'user': user})
else:
return JsonResponse({
'error': 'Not Acceptable',
'available': ['application/json', 'application/xml', 'text/html']
}, status=406)
ASP.NET Core:
// Configure MVC output negotiation at startup.
builder.Services.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
options.RespectBrowserAcceptHeader = true;
}).AddXmlSerializerFormatters();
// Controller: MVC chooses a configured formatter for the object.
[HttpGet("api/users/{id}")]
public IActionResult GetUser(int id)
{
var user = _userService.GetUser(id);
return Ok(user);
}
Go:
Here negotiateMediaType is a project helper, not a Go standard-library function. It must honor media ranges and reject q=0; searching the header with strings.Contains would accept explicitly excluded types.
func userHandler(w http.ResponseWriter, r *http.Request) {
user := getUser(r.URL.Query().Get("id"))
// Project helper: parse media ranges, specificity, wildcards and q weights.
selected := negotiateMediaType(r.Header.Get("Accept"),
[]string{"application/json", "application/xml"})
w.Header().Set("Vary", "Accept")
switch selected {
case "application/json":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
case "application/xml":
w.Header().Set("Content-Type", "application/xml")
xml.NewEncoder(w).Encode(user)
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotAcceptable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Not Acceptable",
"available": []string{"application/json", "application/xml"},
})
}
}
Try It Yourself
Visit our request builder and trigger a 406 response:
- Set method to GET
- Set path to /api/users/1
- Add header
Accept: application/pdf - Click Send request
- See 406 with list of available formats
Try it with curl
Ask a JSON-only API for XML.
curl -i -H 'Accept: application/xml' https://api.example.com/users/1
A JSON-only endpoint could respond:
HTTP/1.1 406 Not Acceptable
content-type: application/json
{"error":"not_acceptable","supported":["application/json"]}
Repeat with -H 'Accept: application/json' and you should get a 200. Many servers ignore Accept and return JSON anyway, so a 200 here is normal, not a bug in your command.
Related Status Codes
- 415 Unsupported Media Type - Request body format not supported
- 300 Multiple Choices - Multiple representation options available
- 400 Bad Request - Malformed request
- 200 OK - Successful content negotiation
Frequently asked questions
What does 406 Not Acceptable mean?
A 406 error means the server cannot produce a response matching the Accept headers sent by the client. The server has the resource but cannot deliver it in the requested format.
How do I fix a 406 error?
Check your Accept, Accept-Language, or Accept-Encoding headers. Broaden them to accept more formats, or verify the server supports the format you are requesting.
What is the difference between 406 and 415?
406 means the server cannot return content in the format you accept. 415 means the server cannot process the format you sent. 406 is about response format; 415 is about request format.
What is content negotiation?
Content negotiation is the process where client and server agree on the best format for a response using Accept headers. The server picks the best match from available representations.
Sources
Related
400 Bad Request
400 Bad Request means the server could not parse your request. Find which layer sent it, fix bad JSON and oversized cookies or headers, and reproduce with curl.
405 Method Not Allowed
405 Method Not Allowed means the URL exists but not for that method. Read the Allow header, then fix redirects turning POST into GET, static hosting and WAFs.
408 Request Timeout
408 Request Timeout means the server gave up waiting for the client to finish sending. Tune nginx, Apache, Node and ALB timeouts and tell it apart from 504.
409 Conflict
409 Conflict means the request clashes with the resource's current state: duplicates, stale versions, locks. Use ETags and If-Match, and handle retries.