# 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.

Source: https://howhttpworks.com/status-codes/406
Last reviewed: 2026-10-05

> **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`](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **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](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **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](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **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](https://github.com/curl/curl/blob/master/lib/http.c), [option documentation](https://curl.se/docs/manpage.html#--fail-with-body).
- **.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](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **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](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java) and [HTTP client source](https://github.com/spring-projects/spring-framework/blob/v6.2.0/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java).

## 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](https://expressjs.com/en/5x/api/response/#res.format).
- **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](https://www.django-rest-framework.org/api-guide/exceptions/#notacceptable).
- **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](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html).
- **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](https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting).
- **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](https://httpd.apache.org/docs/2.4/content-negotiation.html).

## 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](https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383526), [SOAP 1.2 media type](https://www.w3.org/TR/soap12-part2/#httpmediatype).

For a SOAP 1.2 endpoint, this request keeps the input and output media types separate:

```bash
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](https://howhttpworks.com/status-codes/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**

```text
Client requests JSON, server only has XML
Accept: application/json → 406 (only text/xml available)
```

**2. Unsupported Language**

```text
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](https://www.rfc-editor.org/rfc/rfc9110#name-accept-encoding).

```text
Client excludes uncompressed responses, server only has identity
Accept-Encoding: gzip, identity;q=0 → 406 if server refuses the preference
```

**4. Unsupported Charset**

```text
Client requests UTF-16, server only has UTF-8
Accept-Charset: utf-16 → 406 (only utf-8 available)
```

**5. API Version Mismatch**

```text
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
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
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
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):**

```http
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
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
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:**

```text
Client: Accept: application/pdf
Server: Can only produce: application/json, text/html
Result: 406 Not Acceptable
```

**Proactive vs Reactive:**

```text
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
HTTP/1.1 406 Not Acceptable
Vary: Accept, Accept-Language
Content-Type: application/json
```

## Common Mistakes

**Include the available formats**

```http
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain

Not acceptable.
```

**Use 415 for an unsupported request body**

```text
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**

```text
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
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
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:**

```javascript
// 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
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
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"
}
```

<span id="content-negotiation-best-practices"></span>

## Content Negotiation Preferences

**Quality Values:**

```http
Accept: application/json;q=1.0, application/xml;q=0.8, text/html;q=0.5
```

**Wildcard Handling:**

```http
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.

```javascript
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:**

```javascript
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()`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest.get_preferred_type). Use it instead of substring matching, which mishandles `q=0` and wildcards. `Vary: Accept` keeps negotiated responses separate in caches.

```python
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:**

```csharp
// 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.

```go
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](https://howhttpworks.com/tools/playground) 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.

```bash
curl -i -H 'Accept: application/xml' https://api.example.com/users/1
```

A JSON-only endpoint could respond:

```http
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](https://howhttpworks.com/status-codes/415) - Request body format not supported
- [300 Multiple Choices](https://howhttpworks.com/status-codes/300) - Multiple representation options available
- [400 Bad Request](https://howhttpworks.com/status-codes/400) - Malformed request
- [200 OK](https://howhttpworks.com/status-codes/200) - Successful content negotiation
