Troubleshooting

400 Bad Request: find out what the server rejected, and fix it

400 Bad Request means the server refused a malformed or oversized request. Fix cookies that grew too large, HTTP sent to an HTTPS port and broken API calls.

Published ~8 min read

Short answer: 400 Bad Request means the server refused to process the request because something in it was malformed or too large: an oversized cookie, plain HTTP sent to an HTTPS port, a missing Host header, or a body the application could not parse. In a browser, the usual fix is to delete the cookies for that one site. On your own server, read the line under “400 Bad Request” on the error page, then nginx’s error log at info level, which names the exact problem.

see the full answer, not just the status
curl -si https://example.com/path | head -n 20
# the <title> of nginx's error page names the reason, for example
# <title>400 Request Header Or Cookie Too Large</title>

What does the message under 400 Bad Request mean?

nginx uses a few internal codes for specific failures and sends all of them to the client as 400, with the reason as a second line. The page text is your first clue:

Message on the pageWhat happenedFix
Request Header Or Cookie Too LargeOne header line, usually Cookie, is bigger than 8 KB, or all headers together do not fit nginx's buffersVisitor: delete the site's cookies. Owner: raise large_client_header_buffers and shrink the cookies
The plain HTTP request was sent to HTTPS porthttp:// went to a port that expects TLS, usually 443Use https://, or redirect with error_page 497
No required SSL certificate was sentThe server requires a client certificate (ssl_verify_client) and none was sentInstall the client certificate, or drop the requirement
Plain 400 Bad Request from nginxA malformed request: missing, duplicate or invalid Host header, broken request lineThe error log at info level names it
400 with a JSON bodyYour application rejected the input: invalid JSON, a missing field, a disallowed HostRead the response body; it usually names the field

Two neighbours look similar. A request line (method plus URL) longer than one 8 KB buffer gets 414 Request-URI Too Large instead, and Node.js answers oversized headers with 431 Request Header Fields Too Large.

How do I fix 400 Bad Request as a visitor?

Open the same page in a private or incognito window. If it loads there, the cookies your normal window sends to that site are the problem: too many of them, or one that has grown or been corrupted. Delete the cookies for that one site through the site information icon to the left of the address bar, then reload. There is no need to clear your whole browser history.

If the private window fails too, look at the URL: a stray %, a link that was encoded twice, or http:// combined with :443 all produce 400s. Browser extensions that add headers can cause it as well. If none of that helps, the problem is on the server, and only its operator can fix it.

Where does nginx log the reason for a 400?

The access log shows the 400, but not why. nginx writes the reason to the error log at the info level, and the stock Debian and Ubuntu configuration logs only error and above, so those lines never appear. Raise the level while you debug: add info to the error_log line inside the http block, or add the line there if your package keeps error_log at the top of the file. It covers every site without an error_log of its own; if a server block sets one, raise that one instead:

/etc/nginx/nginx.conf · inside the http block
error_log /var/log/nginx/error.log info;
Debian · Ubuntu
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/error.log | grep -E 'client sent|plain HTTP'

Each cause has its own line. These came from a test server:

error.log · info level
client sent too long header line: "Cookie: big=aaaaaaaa…" while reading client request headers
client sent plain HTTP request to HTTPS port while reading client request headers
client sent HTTP/1.1 request without "Host" header while reading client request headers
client sent duplicate host header: "Host: b", previous value: "Host: a"
client sent invalid host header while reading client request headers
client sent too long URI while reading client request line

info is noisy: it also logs every closed keep-alive connection. Remove the info again and reload when you are done. To list the recent 400s from the access log, where the status is the ninth field in the default format:

recent 400s
sudo awk '$9 == 400' /var/log/nginx/access.log | tail -n 20

Cause 1: request header or cookie too large

This is the 400 that ordinary visitors run into. nginx reads request headers into a 1 KB buffer and, when that is not enough, into large_client_header_buffers: by default 4 buffers of 8 KB. A single header line longer than 8 KB gets a 400 with Request Header Or Cookie Too Large, and so does a set of headers that needs more than four buffers. Since nginx 1.19.7 the same directive also governs HTTP/2, so switching protocols does not help.

The usual reasons the headers grow:

  • Analytics, A/B testing and consent cookies set on the parent domain, so every subdomain receives all of them.
  • A session or JWT kept in a cookie that grows with each role or setting added to it.
  • Single sign-on tokens and, on company networks, large Kerberos (Negotiate) headers.
reproduce with curl
# a 9 KB cookie: 400 Request Header Or Cookie Too Large
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Cookie: big=$(head -c 9000 /dev/zero | tr '\0' a)" https://example.com/

# what does a real browser send? In the browser's developer tools, Network tab,
# right-click the failing request, "Copy as cURL", and look at the -H 'cookie: ...' part.

To raise the limit, set it in the http block. If you set it only inside one server block, nginx may apply the default server’s value to connections on that address, and the change seems to do nothing.

/etc/nginx/nginx.conf · inside the http block
large_client_header_buffers 4 16k;
apply and test
sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Cookie: big=$(head -c 9000 /dev/zero | tr '\0' a)" https://example.com/

Buffers are allocated per connection only when needed, so 16k is cheap. If you find yourself going far beyond 32k, fix the cookies instead: keep a session ID in the cookie and the data on the server, and expire cookies nobody reads.

Behind a reverse proxy, the smallest limit in the chain wins. Raise nginx to 32 KB and a Node.js app behind it still answers 431 at 16 KiB. The defaults:

ServerDefault limitAnswer when exceededSetting
nginx8 KB per header line, 4 buffers400 (414 for the request line)large_client_header_buffers
Apache 2.48,190 bytes per header field, 100 fields400LimitRequestFieldSize, LimitRequestFields
Node.js16 KiB for all headers together431--max-http-header-size
IIS (HTTP.sys)16 KB per header, 16 KB for request line plus headers400MaxFieldLength, MaxRequestBytes in the registry

For Node, start it with node --max-http-header-size=32768 server.js. On Windows, the HTTP.sys values live under HKLM\System\CurrentControlSet\Services\HTTP\Parameters and take effect after the HTTP service restarts; Microsoft marks raising them as a memory and attack-surface trade-off. The reverse problem, a backend whose response headers are too big for nginx, is not a 400: it shows up as upstream sent too big header and a 502 Bad Gateway.

Cause 2: the plain HTTP request was sent to HTTPS port

A client spoke plain HTTP to a port that expects TLS. nginx answers 400 with that sentence. Where it comes from:

  • A URL like http://example.com:443/, typed by hand or built by an app that glues scheme and port together.
  • An uptime monitor or health check set to HTTP on port 443.
  • A reverse proxy with proxy_pass http://… pointing at a backend that only speaks HTTPS. If the backend is nginx, its 400 is passed straight through to the visitor; use proxy_pass https://… instead.
reproduce
curl -si http://example.com:443/ | head -n 1
# HTTP/1.1 400 Bad Request

For people who type the wrong scheme, turn the error into a redirect. Internally nginx calls this case 497, and error_page can catch it:

in the server block that listens with ssl
# on the standard port 443
error_page 497 =301 https://$host$request_uri;

# on a non-standard TLS port such as 8443
error_page 497 =301 https://$host:$server_port$request_uri;

The mirror image, HTTPS sent to a plain HTTP port, fails in the client instead: curl reports wrong version number and browsers show ERR_SSL_PROTOCOL_ERROR. On the server it leaves an access-log line with status 400 whose request starts with \x16\x03\x01. That is a TLS handshake. Most of these come from scanners and can be ignored.

Cause 3: a malformed request

HTTP/1.1 requires exactly one valid Host header, and RFC 9112 obliges servers to answer 400 when it is missing, repeated or invalid. Browsers never get this wrong; hand-written clients, device firmware, broken proxies and scanners do. curl can reproduce both cases:

reproduce
curl -si -H 'Host:' http://example.com/ | head -n 1             # Host removed: 400
curl -si -H 'Host: exa mple' http://example.com/ | head -n 1    # invalid Host: 400

Cause 4: the application rejected the request

If the 400 comes with a JSON body or your framework’s error page, nginx passed the request on and the application refused it. The body usually says why, so read it with curl -si. The common mistakes:

  • JSON sent as a form. curl -d labels the body application/x-www-form-urlencoded, and many APIs then fail to parse it. Use --json (curl 7.82.0 and later, so Debian 12 and Ubuntu 24.04 have it), which also sets the Content-Type and Accept headers to JSON.
  • Shell quoting broke the JSON. Wrap the JSON in single quotes, or keep it in a file and send --json @body.json.
  • The application does not accept the Host it sees. By default nginx sends the upstream address ($proxy_host, such as 127.0.0.1:8000) as the Host. Django, with ALLOWED_HOSTS set to your domain, answers that with 400. Pass the real one with proxy_set_header Host $host;, as in the reverse proxy guide.
curl
# wrong: form-encoded body, the API cannot parse it
curl -si -d '{"name":"web-1"}' https://api.example.com/servers

# right: JSON body with Content-Type and Accept set
curl -si --json '{"name":"web-1"}' https://api.example.com/servers
curl -si --json @body.json https://api.example.com/servers

If the error page footer says cloudflare instead of nginx, the request was rejected at Cloudflare’s edge and never reached your server, which is why your logs are empty.

How do I keep 400 errors away?

  • Keep cookies small: an ID in the cookie, the data on the server. Set analytics cookies on the exact host rather than the parent domain where you can.
  • Set large_client_header_buffers once, in the http block, and keep the application server’s limit at least as large.
  • Redirect instead of failing: error_page 497 on TLS ports and a port 80 server that sends everything to HTTPS.
  • Always pass proxy_set_header Host $host; from your reverse proxy.
  • Read the body of every 400 from an API before changing anything. A too-large upload is a different error, 413 Request Entity Too Large.

The paths and defaults above are those of the stock nginx package on a Debian VPS or an Ubuntu VPS, so the commands work as written on a fresh template.

Frequently asked

How do I fix 400 Bad Request in Chrome?

Open the page in an incognito window. If it loads there, delete the cookies for that one site through the site information icon to the left of the address bar, then reload. If it fails in incognito too, the link itself or the server is at fault, so check the URL or contact the site's operator.

What does 'Request Header Or Cookie Too Large' mean?

nginx refused the request because one header line, usually Cookie, is larger than its 8 KB buffer, or all headers together do not fit its four buffers. Visitors fix it by deleting the site's cookies. Server owners can set 'large_client_header_buffers 4 16k;' in the http block and reload nginx, but should also find out why the cookies grew.

Is 400 Bad Request a client or a server error?

In HTTP terms it is a client error: the server says the request itself is malformed, too large or invalid. The cause can still sit on the server side, for example a link that points http:// at port 443 or a reverse proxy that sends the application a Host it does not accept.

Related articles

All troubleshooting