406 Not Acceptable Nginx | Fast Fixes That Work

A 406 Not Acceptable in Nginx happens when your request gets rejected by rules tied to headers, user agents, content negotiation, or security filters.

You load a page, and instead of content you get a blunt status code. No crash, no stack trace, no hint. Just “406 Not Acceptable” and the feeling that the server decided it doesn’t like you.

This error is rarely random. Most of the time, Nginx is doing exactly what it was told to do. The rough part is that the “told to do” part may be buried in config includes, a WAF rule set, a reverse proxy hop, or an app that expects headers your client never sent.

The goal here is simple. Find the rule that blocks the request, confirm why it fires, then adjust it so good traffic gets through while the bad stuff still hits a wall.

406 Not Acceptable Nginx In Real Traffic

A 406 response often shows up in one of two moments. Right after a deploy when a new config lands, or right after a security change when filtering gets stricter. It can also show up only for certain clients, like a mobile app, a bot checker, a payment webhook, or a single browser with privacy extensions.

The “Not Acceptable” wording comes from HTTP content negotiation, where a server can refuse a response format the client asked for. In Nginx land, you also see 406 when rules decide a request looks unsafe or malformed.

When you see it in production, treat it like a sorting problem. Which requests fail, which pass, and what differs between them. You can often isolate the cause in minutes once you compare method, path, headers, and the client identity.

What A 406 Looks Like In Logs

Start with the access log line for a failing request. If the status is 406 and the upstream status is empty, Nginx blocked it before it reached your app. If there is an upstream status, your app or an upstream proxy produced the 406 and Nginx just forwarded it.

If you have request IDs enabled, use them. A single ID that follows the request through Nginx, a proxy, and the app saves a pile of guesswork.

What Triggers A 406 In Nginx

Many sites never emit a 406 until they add a filter layer. That layer can be a WAF module, a ruleset from a CDN, a bot gate, or a strict header policy in your own config. Some apps also return 406 on purpose when they can’t serve the requested content type.

The list below matches the usual culprits that show up on busy WordPress, Laravel, Node, and API stacks behind Nginx.

Trigger What You’ll See Fast Check
WAF rule match 406 only on certain URLs or payloads Check WAF audit logs and rule IDs
Bad or missing headers One client fails, browser works Compare request headers side by side
Content negotiation API returns 406 with Accept mismatch Send Accept: application/json
Method or body limits POST fails, GET works Review size limits and blocking rules
Geo or bot gating Some networks fail, others pass Check allow/deny maps and bot lists

Header And Content Type Mismatches

If your app expects JSON and the client asks for HTML, many frameworks will refuse the request with 406. This happens with APIs where a client sends Accept values that don’t match what the endpoint produces. It also happens when a proxy strips headers and the upstream falls back to a default that your app rejects.

A quick test is to replay the request with curl and set the Accept header explicitly. If the request starts working with an Accept value your app can serve, you’ve found the root cause.

Security Filters That Treat Inputs As Suspicious

WAF rules often fire on patterns like SQL keywords, shell metacharacters, base64 blobs, or odd encodings. That’s good when it blocks attacks, and a headache when it blocks a normal search query, a coupon code, a JSON payload, or a webhook signature.

Some stacks return 406 instead of 403 for these blocks, depending on the module and ruleset. If you use ModSecurity with the Nginx connector, 406 is a common status choice when a rule denies a request.

Fixing Nginx 406 Not Acceptable After A Deploy

Deploy day 406s usually come from a config change that tightened matching. A new include file, a map rule, a rewrite, a header policy, or a WAF update can flip a request from “pass” to “blocked” with no code change at all.

The fastest path is to compare the last known good config to the new one, then target the exact location block, server block, or module where the decision happens.

Trace Where The 406 Is Generated

  • Check Access Log Fields — Confirm whether upstream status is present, and note the request method, URI, and user agent.
  • Review Error Log Context — Look for rule messages, module notes, or “access forbidden by rule” style lines around the same timestamp.
  • Bypass Upstream Temporarily — Hit a static location on the same server to see if the block happens before proxying.
  • Replay With Curl — Send the same path and headers so you can iterate fast without a browser in the loop.

Roll Back One Change At A Time

When you have many moving parts, strip it down. If a WAF update landed, disable that layer for a single test location or a staging host. If a map rule changed, revert only that file and reload. If you changed header rules, revert them and retest the same failing request.

Don’t shotgun random toggles. You want a clean before-and-after that proves what actually caused the block.

Common Deploy-Time Gotchas

  • New Include Order — A later include can override earlier allow rules and make a block win.
  • Map Or Geo Rules — A default value can switch from allow to deny if a key is missing.
  • Stricter Header Filters — A rule that rejects empty referers or unknown agents can trap real users.
  • Proxy Header Changes — Removing Host, X-Forwarded-For, or Accept can change upstream routing and content type behavior.

Step By Step Checks On The Server

Once you’ve confirmed Nginx is the layer producing the 406, use a repeatable checklist. Keep the failing request consistent, change one variable, and log what changes. That way you end with a fix you can defend, not a mystery tweak.

Start With A Clean Reproduction

  • Capture One Failing Request — Copy the exact URL, method, headers, and body if it is a POST.
  • Save A Working Comparator — Grab a request that hits the same server and returns 200, even if the path differs.
  • Diff The Two Requests — Focus on Accept, Content-Type, User-Agent, Referer, Host, and any auth headers.

Turn On Better Logging For The Target Location

On a busy host, default logs can hide the clue you need. Add a temporary log format that includes Accept, Content-Type, upstream status, and request length. Keep it scoped to the server block or location where the failure happens, then remove it after you close the issue.

If you’re using a WAF, enable audit logging and include the rule ID in the output. Without the rule ID, you’re left guessing which pattern triggered the block.

Check Filters That Reject Based On Headers

  • Test Without Accept Header — Some clients send odd Accept values; removing them can show a negotiation bug.
  • Force A Safe Accept Value — Try application/json for APIs and text/html for pages.
  • Set A Standard User Agent — A blank or strange agent can trip bot gates; test with a normal browser agent string.
  • Confirm Host And Scheme — Wrong Host headers or mixed http/https routing can send a request to a block-only server block.

Validate Limits Without Guesswork

Some 406s are misclassified limit hits. A body that exceeds a configured size limit may get handled by a module that returns a nonstandard status. If your failing requests are POSTs, note the request size and compare it to client body settings.

Also check URI length and header size limits if the failure happens on long query strings, large cookies, or signed URLs.

Client Side Causes That Look Like Server Bugs

It’s easy to blame the server, then waste hours editing config when the client is the odd piece. A single client can send headers that trigger blocks, send a content type your app won’t accept, or hit a path with encoded characters that a filter rejects.

This is where request replay pays off. If you can reproduce the 406 from a terminal, you can isolate the delta between a working and failing client with a simple header edit.

Mobile Apps, Webhooks, And Scripts

Apps and scripts often use default headers from their HTTP libraries. Some send Accept values like */* plus extra items, some omit Content-Type on JSON, and some send compressed payloads that confuse filters. Webhooks can include signatures and long headers that trip size limits.

  • Set Content-Type Correctly — JSON posts should send application/json and form posts should send application/x-www-form-urlencoded.
  • Send An Explicit Accept — Ask for what the endpoint serves, not a grab bag that triggers negotiation rules.
  • Match Encoding Expectations — If the client sends gzip, confirm upstream supports it and filters won’t treat it as opaque data.

Browser Extensions And Privacy Tools

Some extensions strip referers, block cookies, or modify user agents. A strict rule that treats missing headers as hostile can punish normal visitors who use privacy tooling. If the 406 appears only in one browser profile, test a clean profile with no extensions.

Also test an incognito window and a different network. If the same path works elsewhere, the server is capable of serving it, and the rejection is tied to request shape.

Encoded Characters And Strict Filters

URLs with encoded slashes, double-encoding, or special characters can trigger rules. Search pages, tag filters, and tracking links often include characters like %2F, %3D, and %26. If your WAF treats these as attack markers, you can end up blocking plain user intent.

Log the raw request URI and the decoded form. Then decide which forms you want to accept, and tune the rule to match that decision.

Hardening Rules Without Blocking Good Users

A quick fix that disables the entire filter layer can stop the bleeding, yet it can also reopen attack paths. The better result is to narrow the rule so it targets the true bad patterns while letting normal traffic pass.

This section is the “make it stick” part. It helps you avoid the same 406 popping up again next week.

Adjust WAF Rules With Evidence

  • Identify The Rule ID — Pull it from the WAF audit log so you know the exact match condition.
  • Confirm The Matched Payload — Find the parameter, header, or body fragment that triggered the block.
  • Scope An Exception Narrowly — Whitelist a single endpoint, parameter name, or content type instead of a broad bypass.
  • Retest With Attack Strings — Confirm the rule still blocks obvious malicious probes after you tune it.

Keep Content Negotiation Simple For APIs

If your API serves JSON, accept JSON. If it serves multiple formats, document them and enforce them clearly. A common pattern is to respond with JSON by default and treat missing Accept as acceptable, since many clients omit it.

If you run multiple versions, keep versioning in the path or a header you control, not in a fragile Accept string that changes per library.

Make A Quick Recovery Checklist For Next Time

When “406 not acceptable nginx” hits again, you want a calm script you can run in order. Save this list in your runbook and keep it close to your deployment notes.

  • Confirm Where The 406 Comes From — Check upstream status to see if Nginx or the app generated it.
  • Grab One Failing Log Line — Note time, URI, method, user agent, and request size.
  • Replay The Request — Use curl with the same headers so you can edit one header at a time.
  • Check WAF Audit Output — Look for rule IDs, matched variables, and the action taken.
  • Compare With A Passing Request — Diff headers and payload shape until the mismatch stands out.
  • Apply A Narrow Change — Adjust a single rule, map, or location block, then reload and retest.
  • Remove Temporary Logging — Clean up debug formats and extra logs after the issue is closed.

When To Escalate Beyond Nginx

If your logs show the upstream generated the 406, shift your focus to the app, framework, or API gateway. Look for content type handling, Accept parsing, and request validation rules. If a CDN sits in front, check its firewall events and bot settings too.

If you’re stuck, reframe the problem into one question: what specific header, parameter, or body fragment flips a request from 200 to 406. Once you have that, the fix is rarely far away.

If you came here after seeing “406 not acceptable nginx” on a live site, you can treat it like a controlled puzzle. Capture, replay, compare, then tune one rule. The moment you stop guessing, the error stops being scary.