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 aSyntaxErrorwithstatus400 andtypeentity.parse.failed. - nginx, before your app sees anything:
400 Request Header Or Cookie Too Largewhen headers outgrowlarge_client_header_buffers, andThe plain HTTP request was sent to HTTPS port. - Django, when the
Hostheader isn’t inALLOWED_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