How HTTP Works
4xx · Client error
< HTTP/1.1 406 Not Acceptable

406 Not Acceptable

The server cannot produce a response matching the client's Accept headers. Learn about content negotiation and how to handle format mismatches.

Reviewed 9 min readintermediate2 sourcesTry itMarkdown
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

TL;DR: 406 Not Acceptable means the server cannot return a response in a format your request accepts. Check Accept first, 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; inspect error.response.status, error.response.data and error.response.headers. Axios constructs this message in settle.
  • fetch: the promise resolves when the HTTP response arrives. response.status === 406 and response.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 from response.raise_for_status(), not from requests.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 -i shows headers and the error body without fail mode; --fail-with-body keeps 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 from EnsureSuccessStatusCode(); inspect the HttpResponseMessage first when you need its body. Runtime message template.
  • Spring: WebClientResponseException.NotAcceptable with message 406 Not Acceptable from GET https://api.example.com/resource for WebClient.retrieve(). RestTemplate’s default error handler uses HttpClientErrorException.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 matching Accept media type and has no default callback. Add the missing representation or a deliberate fallback; inspect req.accepts(['json', 'xml']) before selecting a format. Express response API.
  • Django REST Framework: none of the view’s renderers can satisfy Accept, so DRF raises NotAcceptable with status 406. Check renderer_classes or DEFAULT_RENDERER_CLASSES; a JSON-only view needs Accept: application/json. DRF exceptions.
  • Spring Boot / Spring MVC: HttpMediaTypeNotAcceptableException maps to 406. Compare the controller’s produces media types with the request’s Accept, then check the configured message converters. An XML response needs an XML-capable converter. Spring resolver mappings.
  • ASP.NET Core MVC: with ReturnHttpNotAcceptable = true, an ObjectResult returns 406 when no output formatter can satisfy Accept. Register AddXmlSerializerFormatters() when you intend to serve XML, or request JSON. Browser Accept headers are ignored by default unless RespectBrowserAcceptHeader is enabled. Microsoft formatting documentation.
  • Apache: mod_negotiation, including Options MultiViews, cannot choose an acceptable file variant. Check the variant media types and language metadata; use Options -MultiViews if 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

CodeMeaningIssueSolution
406Not acceptableResponse format unavailableRequest different format
415Unsupported media typeRequest format unacceptableSend different format
400Bad requestMalformed requestFix request syntax
404Not foundResource doesn’t existCheck 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:

  1. Set method to GET
  2. Set path to /api/users/1
  3. Add header Accept: application/pdf
  4. Click Send request
  5. 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.

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

  1. MDN Web Docs: 406 Not Acceptabledeveloper.mozilla.org
  2. RFC 9110: 406 Not Acceptablerfc-editor.org

Keep going

Browse /search