HTTP 400 Bad Request

Bad Request

HTTP 400 is Bad Request: the server won’t process the request because of something it sees as the client’s fault. RFC 9110 section 15.5.1 names “malformed request syntax, invalid request message framing, or deceptive request routing” as examples. The client shouldn’t send the same request again unchanged.

What sends it

  • Body parsers, for JSON that doesn’t parse. Express’s express.json() hands the error handler a SyntaxError with status 400 and type entity.parse.failed.
  • nginx, before your app sees anything: 400 Request Header Or Cookie Too Large when headers outgrow large_client_header_buffers, and The plain HTTP request was sent to HTTPS port.
  • Django, when the Host header isn’t in ALLOWED_HOSTS.
  • Your own handlers, for a missing or unreadable parameter.

The oversized cookie is a classic: one bloated cookie on a domain, and every request to that domain fails with 400 until the cookie is cleared or expires.

400 or its neighbours

{"qty": 2,}               400  doesn't parse: trailing comma
Content-Type: text/xml    415  Unsupported Media Type
{"qty": "two"}            400 or 422  parses, wrong type
{"qty": -2}               400 or 422  parses, breaks a rule
valid, but a duplicate    409  Conflict
no or expired token       401  Unauthorized

RFC 9110 doesn’t limit 400 to syntax; any client error qualifies, and Stripe’s and Google’s APIs send 400 for invalid values. 422 Unprocessable Content is the more specific answer for a body that parses but fails validation, and GitHub’s API, Rails and FastAPI use it. FastAPI goes further and answers 422 even for broken JSON. Either convention is defensible; pick one per API and document it.

A body that helps

RFC 9457 problem details give clients something to parse instead of a bare status:

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

{"type": "https://example.com/probs/invalid-json", "title": "Body is not valid JSON",
 "status": 400, "detail": "The body ends with a trailing comma"}

400 isn’t heuristically cacheable, and it isn’t worth retrying: the same bytes get the same answer.

In your stack

app.use(express.json());
// ...routes...
app.use((err, req, res, next) => {
  if (err.type === 'entity.parse.failed') return res.status(400).json({ error: 'invalid_json' });
  next(err);
});
abort(400, description="qty must be a number")          # Flask
return HttpResponseBadRequest("qty must be a number")   # Django
if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
    http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
    return
}
large_client_header_buffers 4 16k;    # the default is 4 8k

Try it in the HTTP Status Codes

400 Bad Request

  1. 400Bad Request4xx Client error

    The server cannot make sense of the request: malformed syntax, an unreadable header, or a body that is not what it claims to be.

    When to use it

    Any input the endpoint cannot parse at all, including a broken JSON body or a bad query parameter.

    Common mistakes

    Using it as a catch-all for anything the user got wrong — well-formed but nonsensical values are 422 — and returning it for a fault on the server’s side.

    Not cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.