# 400 Bad Request

> 400 Bad Request means the server could not parse your request. Find which layer sent it, fix bad JSON and oversized cookies or headers, and reproduce with curl.

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

> **TL;DR:** 400 Bad Request means the server, or a proxy in front of it, could not parse or accept the request as sent: broken JSON, an invalid header, an oversized cookie, or a body that fails validation. Find out which layer returned it, then replay the exact request with `curl -v` and bisect.

RFC 9110 section 15.5.1 covers perceived client errors, including malformed syntax, invalid message framing and deceptive routing. Because it is a catch-all, the response body and the `Server` header tell you more than the status line.

The messages and response bodies here are illustrative. Client messages use `https://api.example.com/resource`; reason phrases and URLs can vary.

```http
HTTP/1.1 400 Bad Request
Server: nginx
Content-Type: text/html

<html><head><title>400 Bad Request</title></head>
<body><center><h1>400 Bad Request</h1></center>
<center>Request Header Or Cookie Too Large</center>
<hr><center>nginx</center></body></html>
```

## What you see in your client

- **Axios:** `AxiosError: Request failed with status code 400` with the default status handling. Inspect `error.response.data` for the server's explanation. [Axios source](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves with `response.status === 400` and `response.ok === false`. Check the status before reading the body; HTTP errors do not enter `catch` automatically. [MDN fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **Python requests:** `requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.example.com/resource` when you call `response.raise_for_status()`. [Requests source](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **curl -f:** `curl: (22) The requested URL returned error: 400`. Use `curl -i` without `-f` while inspecting the error body. [curl source](https://github.com/curl/curl/blob/master/lib/http.c).
- **.NET:** `HttpRequestException` with the English message `Response status code does not indicate success: 400 (Bad Request).` after `EnsureSuccessStatusCode()`. [Runtime message template](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring WebClient:** `WebClientResponseException$BadRequest: 400 Bad Request from POST https://api.example.com/resource` with `retrieve()`. Read `getResponseBodyAsString()`. [Spring 6.2 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).
- **Spring RestTemplate (6.2):** `HttpClientErrorException$BadRequest: 400 Bad Request on POST request for "https://api.example.com/resource": [no body]` with the default error handler and an empty response body. Read the exception body when present. [Message builder](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java), [exception subclasses](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/web/client/HttpClientErrorException.html).

## Who sent it?

| Signal | Likely source |
| --- | --- |
| HTML page with `<center>nginx</center>` and a reason line | nginx rejected it before your app ran |
| `Bad Request - Invalid Hostname` or `Bad Request - Invalid URL` | IIS / HTTP.sys |
| `Bad Request: Your browser sent a request that this server could not understand.` | Apache httpd |
| `CF-Ray` header and a Cloudflare-styled page | Cloudflare edge |
| `Server: awselb/2.0` | AWS ALB (malformed request or header limits) |
| JSON body with field-level errors | Your application's validation layer |
| `SyntaxError: Expected double-quoted property name in JSON at position 17` (older Node: `Unexpected token } in JSON`) | `express.json()` body parser |

nginx reason lines you will see in the page body, with the matching error-log text:

- `Request Header Or Cookie Too Large`: `large_client_header_buffers` (default `4 8k`) was exceeded, usually by accumulated cookies. Log: `client sent too long header line` or `client sent request with too large header`.
- `The plain HTTP request was sent to HTTPS port`: someone spoke HTTP to a `listen 443 ssl` port. Fix the scheme or port.
- `No required SSL certificate was sent`: `ssl_verify_client on` and the client presented no certificate.

## Fix it, most common first

1. **Malformed JSON.** A trailing comma, single quotes, comments, or an unescaped newline. Validate with `jq . < body.json`. In shells, quoting is the usual culprit: `-d '{"a":1}'` works, `-d "{"a":1}"` does not.
2. **Content-Type does not match the body.** curl's `-d` sends `application/x-www-form-urlencoded`. Form parsers can leave JSON fields missing and trigger validation; JSON-only endpoints can instead return [415](https://howhttpworks.com/status-codes/415). Add `-H 'Content-Type: application/json'`.
3. **Cookies too big.** Clear cookies for the domain. If it is your site, find what is writing large cookies: the combined `Cookie` header has to fit the server's per-header buffer.
4. **Invalid Host or URL characters.** Unencoded spaces, `%` not followed by two hex digits, or non-ASCII bytes in the request line.
5. **Framing guards.** Both `Content-Length` and `Transfer-Encoding` present, or conflicting duplicate `Content-Length` values. Servers reject or close these (RFC 9112 section 6.3) to block request smuggling; fix the client or the proxy that rewrites them.
6. **Schema validation failure.** Missing required field or wrong type. Many APIs use [422](https://howhttpworks.com/status-codes/422) for this instead.

## Common causes by stack

- **ASP.NET Core controllers:** `[ApiController]` returns 400 automatically for model validation failures, with an `errors` dictionary in `ValidationProblemDetails`; read those keys before changing the serializer. [Microsoft API documentation](https://learn.microsoft.com/en-us/aspnet/core/web-api/#automatic-http-400-responses).
- **Django REST framework:** `ParseError` and serializer `ValidationError` both default to 400; distinguish broken JSON from a field validation message in the response body. [DRF exceptions](https://www.django-rest-framework.org/api-guide/exceptions/).
- **Rails:** `ActionController::ParameterMissing` and `ActionDispatch::Http::Parameters::ParseError` map to 400; inspect the required parameter and send the expected JSON nesting. [Rails exception mappings](https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb).
- **API Gateway REST APIs:** `BAD_REQUEST_BODY` and `BAD_REQUEST_PARAMETERS` default to 400 when request validation fails; compare the payload with the method's model and required parameters. [Gateway response types](https://docs.aws.amazon.com/apigateway/latest/developerguide/supported-gateway-response-types.html).

## Fix by stack

### nginx

```nginx
http {
    client_header_buffer_size   4k;
    large_client_header_buffers 4 16k;   # number and size; one header line must fit in one buffer
}
```

### Express / Node.js

`express.json()` throws on invalid JSON, and the default error handler answers 400 with an HTML stack trace outside production. Return JSON instead:

```javascript
app.use(express.json({ limit: '1mb' }))

app.use((err, req, res, next) => {
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'invalid_json', message: err.message })
  }
  next(err)
})
```

Node's HTTP parser tries to return 400 and close the socket for parse errors, but `HPE_HEADER_OVERFLOW` gets [431](https://howhttpworks.com/status-codes/431) by default. These errors reach `clientError` before your request handler; inspect `err.code`. [Node HTTP documentation](https://nodejs.org/api/http.html#event-clienterror).

### Spring and Django

Spring returns 400 for `HttpMessageNotReadableException` (`JSON parse error: Unexpected character`) and for `MethodArgumentNotValidException` when `@Valid` fails. Django turns `SuspiciousOperation` into 400; the classic one is `Invalid HTTP_HOST header: 'x'. You may need to add 'x' to ALLOWED_HOSTS.` with `DEBUG = False`.

### Cloudflare and AWS

Cloudflare documents 400 for requests containing both `Content-Length` and `Transfer-Encoding`; inspect the framing headers added by each proxy. [Cloudflare 400 guidance](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-400/).

An ALB returns 400 for malformed requests, an incomplete body, or headers exceeding its documented limits: 16 K for a request line, 16 K for one header and 64 K for all request headers. Compare the browser's cookies with a cookie-free request. [ALB troubleshooting](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html).

## Reproduce and verify

```bash
# Bad JSON: a well-behaved parser answers 400
curl -sv -X POST https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "John",}' 2>&1 | grep -E '^(>|<) '

# Oversized cookie: nginx answers 400 "Request Header Or Cookie Too Large"
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/ \
  -H "Cookie: junk=$(head -c 9000 /dev/zero | tr '\0' a)"
```

If the request works from curl but fails from the browser, diff the headers: the browser is sending cookies, `Origin`, or a body encoding that curl is not.

## Write a useful 400 body

RFC 9457 `application/problem+json` is the standard shape for API errors:

```http
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/invalid-json",
  "title": "Request body is not valid JSON",
  "status": 400,
  "detail": "Unexpected token '}' at position 45"
}
```

Use 404 for missing resources, 401/403 for authentication or permission failures, and 5xx for server faults.

## 400 vs its neighbors

Use 400 when the request cannot be parsed (syntax or framing) and [422 Unprocessable Content](https://howhttpworks.com/status-codes/422) when it parses but fails semantic validation. Many APIs return 400 for both; consistency matters more than the choice. A header block that is too large has its own code, [431](https://howhttpworks.com/status-codes/431), though nginx still uses 400 for it. See also [401](https://howhttpworks.com/status-codes/401), [403](https://howhttpworks.com/status-codes/403) and [404](https://howhttpworks.com/status-codes/404). You can trigger a 400 safely in the [request builder](https://howhttpworks.com/tools/playground).

See also the comparison [400 vs 422](https://howhttpworks.com/compare/400-vs-422): which of the two to return for validation errors and what common frameworks default to.
