Fluxtail
Log Management Guides

Error Code 406 Not Acceptable: A Practical Guide

Troubleshoot and fix the HTTP error code 406 Not Acceptable. Learn common causes like content negotiation, server configs, and WAFs, and how to fix them.

2026-06-15 error code 406 http status codes content negotiation server troubleshooting log analysis

Your alert fires a few minutes after a deployment. The dashboard shows a spike in 406 responses. Nothing else looks obviously broken. Latency is normal. CPU is boring. The upstream dependency is healthy. Yet clients are failing hard.

This is the kind of incident that burns time because it looks smaller than it is. A 406 can come from an API client asking for a format the server won't send. It can also come from a proxy, a CMS rule, or a security layer that decided the request looked wrong enough to reject. If you treat it as “just an Accept header problem,” you can lose an hour in the wrong logs.

The practical goal is simple. Reproduce the request, identify who emitted the 406, compare what the client asked for with what the stack can serve, and restore a sane fallback path where appropriate. Fast incident response depends on that kind of methodical narrowing, which is why reducing mean time to resolution during production issues matters more than memorizing status code trivia.

Table of Contents

That Sudden Spike of 406 Errors

A production 406 spike usually starts with confusion. One client library fails, but browser traffic might still work. One partner integration reports errors, but your health checks stay green. Someone says, “Maybe it's a bad deploy.” Someone else says, “Maybe the API vendor changed something.” Both are plausible.

The first useful mental model is this: error code 406 is a negotiation failure, not proof of server death. The request reached something. That something understood enough about the request to reject the representation being asked for. That's why incidents like this often feel slippery. The service can be up and still be unusable to a subset of callers.

What the incident often looks like

In practice, these incidents tend to show up in a few recognizable forms:

  • A new client release tightened headers: An SDK, mobile app, scraper, or integration begins sending a stricter Accept value than before.
  • A server change removed a fallback: The app used to answer with a default representation. Now it refuses anything that doesn't match exactly.
  • An edge layer changed behavior: A CDN, reverse proxy, or WAF starts normalizing, stripping, or rejecting requests.
  • A security rule started firing: The response is technically 406, but the actual root cause lives in filtering logic, not content negotiation.

Operational rule: If only one class of clients is failing, compare their headers before you compare the whole deployment.

That distinction matters because it changes the order of investigation. If every client is failing, start at the server and edge. If one integration is failing, start with the exact request shape that integration sends. If bots, scrapers, or unusual user agents are involved, check the security layer earlier than you normally would.

A junior engineer's first instinct is often to grep app logs for 406 and stop there. That's not enough. In a modern stack, the response may have been generated by Nginx, Apache, an API gateway, a WAF, or framework middleware before your application code had a chance to do anything useful.

Understanding HTTP Content Negotiation

At the protocol level, HTTP content negotiation is the process by which a client tells the server what kind of response it can accept, and the server decides what representation to return. MDN documents 406 Not Acceptable as meaning the server “could not produce a response matching the list of acceptable values” in the request headers, including cases where it won't provide a default representation, in its reference on HTTP 406 and content negotiation.

An infographic explaining HTTP content negotiation using a coffee shop analogy and technical steps for developers.

The coffee shop version

Think of a request like ordering coffee in a shop with a very strict menu.

You ask for “an oat milk cappuccino, extra dry, in French only.” The barista can make coffee, but not that exact combination, and they refuse to guess a substitute. You both understand that coffee exists. The failure is in matching your stated preferences to what the shop is willing to serve.

HTTP does the same thing with headers:

  • Accept tells the server which media types the client wants, such as JSON or HTML.
  • Accept-Language tells it which language variants are acceptable.
  • Accept-Charset communicates acceptable character encodings.
  • Accept-Encoding signals which content encodings the client can decode.

Why this still matters in modern systems

A lot of engineers associate negotiation with old browser behavior. That's incomplete. The same mechanism shows up in API clients, cloud integrations, gateways, and automation tooling. A single mismatched header can trigger a hard failure even when the backend itself is healthy.

Cloudflare's explanation of what triggers a 406 response is useful here. The server returns 406 when it can't provide the resource in a format that matches negotiation headers like Accept, Accept-Language, or Accept-Charset. Cloudflare also notes that a server can avoid 406 by serving a less-preferred representation instead of rejecting the request outright.

If your service can safely fall back, a default representation is often better for reliability than a strict rejection.

That doesn't mean “always ignore the client's preferences.” It means you should choose strictness deliberately. Public APIs, browser-facing endpoints, and internal service-to-service calls often need different negotiation behavior. The wrong default can break compatibility. The wrong strictness can break uptime.

A good debugging habit is to stop thinking of these headers as passive metadata. They are part of the request contract. If a client says Accept: application/json and your endpoint only emits application/ld+json, you don't have a transport problem. You have a contract mismatch.

Four Common Causes of 406 Errors

Most guides stop at “bad Accept header.” That's one cause, not the whole picture. In production, error code 406 usually comes from one of four buckets. The trick is identifying which bucket owns the response before you start changing code.

An infographic showing the four most common causes of the 406 Not Acceptable error in web development.

Restrictive client headers

This is the classic case. A client asks for a representation the server doesn't provide.

Microsoft documents a concrete example in its Azure Data Factory support thread on a 406 failure. The Copy Data activity sent the default Accept: application/json, but the target API expected JSON-LD or another specialized media type. The service was reachable. The negotiation failed.

This happens more often than teams expect because many tools choose defaults. SDKs do it. ETL tools do it. Browser-based test clients do it. So do scripts copied from old examples.

A few patterns show up repeatedly:

  • Overly narrow Accept values: The client asks for one exact media type and rejects everything else.
  • Unexpected language constraints: Accept-Language is present because of browser defaults or copied headers.
  • Encoding or charset mismatch: Less common, but still real in older apps and strict servers.

Server configuration that refuses fallback

Sometimes the client request is reasonable, but the server is configured too narrowly. The application might only declare one Content-Type. The web server might be missing MIME mappings. Framework middleware may reject anything that doesn't match an expected type exactly.

The scenario of “works in browser, fails in automation” often arises. Browsers tend to send broad, forgiving Accept headers. API tools and machine clients often don't. If your server only behaves under forgiving negotiation, the implementation isn't as dependable as you think.

Middleboxes that alter request semantics

A reverse proxy, CDN, or gateway can break negotiation without meaning to. Header normalization, header stripping, rewrites, or default additions can all change what reaches the origin.

This is one reason edge reproduction matters. A request that succeeds directly against the app may fail through the public path. If you only test origin behavior, you'll miss the component that's generating the 406.

Layer Typical failure mode What to verify
Reverse proxy Removes or rewrites Accept Incoming and forwarded headers
CDN or gateway Applies policy based on content type Edge logs and route behavior
Ingress or service mesh Alters compression or negotiation-related headers Effective request at the app boundary

Security and filtering layers

This is the cause too many teams skip. A 406 isn't always about representation selection. It can be a deliberate block emitted by a security control.

FastComet notes in its guide to 406 responses and filtering layers that ModSecurity rules can return 406, and that excessive crawling can also trigger this class of response. That's the practical gap in many explainers. The right question isn't always “what media type did the browser ask for?” Sometimes it's “which edge rule rejected the request and why?”

Don't assume the application owns every 406. Security products often use standard HTTP codes to signal policy enforcement.

When requests look suspicious, malformed, unusually repetitive, or scraper-like, WAFs may emit 406 before the app sees anything. In those incidents, changing controller code won't help. You need the rule ID, the matched condition, and the exact request pattern that triggered it.

How to Debug a 406 Not Acceptable Error

When a 406 lands in production, don't start by editing config files. Reproduce first. Then prove which component returned the response. Then compare the request headers with the supported output. That order saves time.

A male programmer working on code across multiple monitors and a laptop at his office desk.

Proxidize makes the right operational point in its write-up on server-side policy and configuration behind 406 errors. A 406 is often a server-side policy or configuration failure rather than a transport problem. The practical fix is to compare incoming request headers against what the server is willing to serve, then confirm that in logs and configuration.

Start with the raw request

Use curl because it makes the negotiation visible.

curl -v https://your-endpoint.example/resource

That gives you the raw response headers and status line. If the failing client is known, mirror its headers as closely as possible.

curl -v https://your-endpoint.example/resource \
  -H 'Accept: application/json'

Then widen the request on purpose.

curl -v https://your-endpoint.example/resource \
  -H 'Accept: */*'

If the strict request fails and the broad one succeeds, you just proved this is a negotiation problem. If both fail the same way, look harder at routing, filtering, or app logic. If curl to the origin works but the public endpoint returns 406, the edge path is involved.

A similar mindset helps in other classes of incidents too, especially when comparing app, proxy, and gateway behavior during server-side failure analysis.

Check the logs that actually own the response

Don't stop at application logs. A 406 may never touch the app.

Look in this order:

  1. Edge or CDN logs
  2. WAF or ModSecurity logs
  3. Reverse proxy access and error logs
  4. Application access logs
  5. Application framework logs

Match by timestamp, path, method, and user agent. If you have a request ID, use it. If you don't, correlate on the narrowest time window possible.

Useful things to look for include:

  • A WAF rule hit: Request blocked before upstream handling
  • An explicit return rule: Proxy or server config emits 406
  • Missing upstream app entry: The request never reached the app
  • Content-type negotiation branch: Framework rejected the requested representation

Here's a quick triage view:

Evidence Likely owner
Access log shows 406, app log silent Proxy, edge, or WAF
WAF log shows matched rule Security layer
App log shows unsupported representation Application code
Origin succeeds, public endpoint fails Gateway, proxy, or CDN

Before you dig into deeper traces, watch this breakdown of the status code and common fixes.

Compare request headers against supported output

Once you know who returned the 406, compare two things side by side:

  • What the client declared acceptable
  • What the server, proxy, or framework is configured to serve

That sounds obvious, but teams often compare only one side. They inspect the request and assume the server should handle it, or they inspect server config and assume clients should be flexible. You need both.

Field note: The fastest wins come from reducing the problem to one failing header and one responsible component.

For application endpoints, verify the response media types your framework advertises. For proxies, verify whether header-based routing or explicit return 406 logic exists. For security tools, find the exact policy, not just the status code. If a WAF emitted the response, the fix may be rule tuning, exclusions, or request normalization rather than content negotiation changes.

Fixing 406 Errors on Common Servers and Frameworks

Once you've isolated the owner, the fix is usually straightforward. The hard part was attribution. Here, the trade-off is between strict correctness and operational resilience. If the endpoint must enforce an exact representation contract, keep it strict. If clients reasonably expect fallback behavior, add it deliberately.

Apache and Nginx fixes

On Apache, start with .htaccess and any virtual host rules that inspect request headers. Look for rewrite conditions, content negotiation directives, and security modules that might return 406. Also verify MIME type mappings if static or generated content types are involved.

A practical Apache checklist:

  • Inspect rewrite rules: Header-based RewriteCond logic can force 406 for specific paths.
  • Review AddType mappings: If the server doesn't know how to label a response, negotiation can fail upstream.
  • Check ModSecurity behavior: If the request never reaches the app, the WAF may be the source.

On Nginx, review nginx.conf, site config includes, and any conditional logic around request headers. Teams sometimes implement exact media-type checks with if blocks or route-specific returns. Also confirm mime.types is present and loaded as expected.

Application layer fixes

Framework behavior matters because many frameworks default to strict matching once you enable content negotiation helpers.

In Node.js with Express, prefer explicit format handling over ad hoc header checks:

app.get('/resource', (req, res) => {
  res.format({
    'application/json': () => {
      res.json({ status: 'ok' });
    },
    'text/html': () => {
      res.send('<p>ok</p>');
    },
    default: () => {
      res.type('application/json');
      res.json({ status: 'ok' });
    }
  });
});

That default branch is the operationally important part if fallback is acceptable. Without it, you're more likely to produce a hard failure.

In other frameworks, the same principle applies:

  • Declare what you can produce: Make response types explicit.
  • Add a fallback intentionally: Only if clients can safely consume it.
  • Log negotiated outcomes: So future incidents don't require guesswork.

A strict API is fine. An accidentally strict API is where incidents come from.

Proxy and edge fixes

If the issue lives above the app, fix it there. Don't patch application code to compensate for a proxy that mangles headers.

Review these areas:

  • Header forwarding: Ensure Accept, Accept-Language, and related headers arrive intact when they should.
  • Edge rules: Look for route-specific policies that reject by path, method, or user agent.
  • Compression settings: If Accept-Encoding is involved, verify the edge and origin agree on supported behavior.
  • Crawler and bot controls: Excessively aggressive filtering can emit 406 for traffic that isn't malicious.

The less discussed but often decisive angle is the filtering stack. As noted earlier, security layers can return 406 for reasons unrelated to normal content negotiation. When that happens, the right fix is usually one of these:

  • Tune the rule
  • Add a scoped exclusion
  • Normalize the request pattern
  • Adjust bot or crawler policy

What doesn't work is guessing. If you're not holding the exact rejected request and the exact component that rejected it, you're still in incident discovery, not incident resolution.

Proactive Monitoring for 406 Spikes with Fluxtail

A 406 incident gets expensive when the signal is buried. The logs exist, but they're split across Nginx, the app, and whatever sits at the edge. Someone tails one file. Someone else looks at a dashboard. Another engineer asks in chat whether there was a deploy. That workflow is how simple negotiation failures turn into long calls.

Screenshot from https://fluxtail.io

Make 406 visible before users file tickets

For this class of incident, the first win is centralizing the raw evidence. That means routing logs from proxies, applications, WAFs, and collectors into streams that are readable under pressure. When the streams are separated cleanly, a 406 spike stops being a vague symptom and becomes a pattern you can inspect.

What you want operationally is:

  • A live tail for active incidents: See 406s arrive in real time with path, host, stream, and message.
  • Named streams by component: Keep proxy, app, and security events distinct.
  • Alerting on status patterns: Notify on unusual bursts of 406s before support tickets pile up.
  • Fast pivoting by request traits: Filter on path, user agent, or deployment window.

That kind of workflow is where live tail for incident response is useful. During a spike, being able to watch only the relevant streams cuts out the usual noise from healthy services and unrelated warnings.

A practical alert for 406s doesn't need to be fancy. Watch for a spike by stream, then group by path or user agent. If the pattern is isolated to one client family, you likely have a negotiation mismatch or bot filtering event. If it aligns with a config rollout, start at the edge and deployment diff.

Use chat driven investigation during incidents

The next bottleneck is query speed. During an incident, engineers don't want to handcraft every search while context-switching across terminals and dashboards. Chat-driven log exploration is useful here because the first questions are usually plain English anyway:

  • Which paths are returning 406 right now?
  • Did these start after the last deploy?
  • Are only certain user agents affected?
  • Did the WAF stream emit matching events?

When logs, analytics, and AI-assisted querying live in the same workflow, the investigation gets tighter. You spend less time moving data around and more time testing real hypotheses. That's especially helpful for 406s because the root cause often sits between layers, not inside a single service.

The best 406 investigation flow answers one question quickly: who rejected what, and under which request conditions?

That's what proactive observability should do for you. Not just retain logs. It should make this class of interoperability failure obvious, attributable, and short-lived.


If your team wants a cleaner way to catch and investigate error code 406 incidents, Fluxtail gives you centralized logs, readable live tail, stream-based routing, alerts, and built-in AI chat in one place. It fits the way SREs debug production issues: start with the raw events, isolate the responsible layer, and move from spike detection to root cause without juggling tools.