correctness

Returning 406 Not Acceptable

When an Accept header genuinely can't be satisfied, 406 is the right response. The common mistake is returning it too eagerly.

406 Not Acceptable is the HTTP response code for I understood your request, but I cannot produce a response in any of the formats you’ll accept. It’s defined in RFC 9110 §15.5.7.

When to return 406

Return 406 when every representation you can produce has either:

  • no match in the client’s Accept list, or
  • a match with q=0 (explicitly rejected)

Concrete scenarios:

  • Client sends Accept: application/pdf, your server can only do HTML and Markdown → 406.
  • Client sends Accept: text/markdown;q=0, your server only has Markdown → 406.

When not to return 406

Most implementation bugs happen here: returning 406 too aggressively. Do not 406 when:

  • The Accept header is missing. That means “no constraint,” not “nothing works.” Serve your default.
  • Accept is */*. Same as missing — serve your default.
  • One of your representations didn’t match, but another did. Pick the one that matches.
  • You’re not sure. A spec-correct default (usually HTML) is better than 406 for user experience.

The rule: 406 means I tried all my options and none work for you. It should be rare.

What to put in the response body

RFC 9110 recommends that a 406 response should include a body that lists the available representations, so the client can pick one and retry:

HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8

This resource is available in:
- text/html
- text/markdown

You requested: application/pdf

A human-readable plain-text body is the easiest. Some implementations use the Link header with rel="alternate" entries, but plain text is more broadly useful.

Status class, caching, and retries

  • 406 is a client error (4xx). That signals the client sent a request the server can’t satisfy.
  • Don’t cache 406 aggressively. The Accept header is request-specific; the same URL may return 200 for a different client. Vary: Accept applies here too, or just Cache-Control: no-store for 406 responses.
  • Agents should not retry without changing Accept. If an agent sees a 406, it knows the next request needs a different Accept value.

Should you fall back silently instead?

There’s a pragmatic argument for always returning a representation rather than 406, even if it doesn’t match Accept. Some sites do this for resilience — better to serve HTML than to error.

The tradeoff: silent fallback masks a real mismatch. If an agent asked for Markdown and got HTML with Content-Type: text/html, the agent may treat the response as wrong format and either ignore it or mis-parse it. A clean 406 is more honest.

The site’s recommendation: return 406 when you genuinely can’t satisfy the request, and keep your default narrow enough that 406 is rare.