Understanding HTTP 400 Bad Request: Malformed Syntax, the 400 vs 422 Line, and When Generic Is Correct
400 Bad Request is the junk drawer of HTTP. Because it's the first client-error code and the most generic one, APIs throw it at everything — failed validation, missing permissions, expired tokens, business-rule violations — until "400" tells the client nothing beyond "you did something wrong, good luck." But the spec is more precise than its reputation: 400 means the server can't even parse your request — malformed JSON, broken framing, garbage syntax. If the server understood the request and merely disliked the data, a different code (usually 422) says that better. Let's go through what 400 actually covers, where the line between 400 and 422 sits, when falling back to a generic 400 is the right call, and how to return one that clients can actually do something with.
What Is a 400?
A 400 means the server refuses to process the request because something about the request itself is broken — its syntax, its framing, or its routing.
The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). — RFC 9110, Section 15.5.1
In plain English: "I can't make sense of what you sent me." The request never got far enough to be judged on its merits — the server choked on the envelope, not the letter. Note the RFC's examples: malformed syntax, invalid framing, deceptive routing. They're all about the shape of the request, not the meaning of the data inside it.
The other thing the RFC's wording buys you: "cannot or will not." 400 is also HTTP's sanctioned generic client error — the code you reach for when the client is clearly at fault but no more specific 4xx fits. More on when that's legitimate below.
400 Is About Syntax, Not Semantics
This is the distinction the code turns on, so it's worth stating bluntly. When a request arrives, the server effectively asks two questions in order:
- Can I parse this? Is the JSON valid, are the headers well-formed, is the required structure present at all? Failing here is 400's domain.
- Does this make sense? The request parsed fine, but the email address has no
@, the end date is before the start date, the quantity is negative. Failing here is 422's domain.
The mental model that keeps it straight:
- 400 = "I can't even read this." — The request is structurally broken. The server never got to look at your data because it couldn't decode it.
- 422 = "I read it, and it doesn't make sense." — Syntax fine, content type fine, but the data fails validation or business rules.
A quick litmus test: could a JSON linter catch the problem? Trailing comma, unclosed brace, string where the parser expected an object — 400. Would it take your application's rules to catch it — an unknown country code, a password that's too short? That's 422.
That said, honesty requires a caveat: plenty of respectable APIs use 400 for all client-side errors, validation included, and that's a defensible convention — 422 only entered the core HTTP spec's orbit recently (it was born in WebDAV's RFC 4918 and is now blessed by RFC 9110, Section 15.5.21). What's not defensible is mixing conventions: if your API returns 422 for a bad email on one endpoint and 400 for the same thing on another, clients can't branch on your status codes at all. Pick a line and hold it everywhere.
400 vs 422 vs 415 vs 413
Four codes cover "there's a problem with the request body," and they answer different questions:
| Code | Meaning | Use when |
|---|---|---|
| 400 Bad Request | Request is malformed | The body can't be parsed at all (invalid JSON, broken syntax), or no more specific 4xx applies |
| 422 Unprocessable Entity | Parsed fine, data invalid | Syntax is valid but the content fails validation or business rules |
| 415 Unsupported Media Type | Wrong format entirely | The Content-Type is one you don't accept (XML sent to a JSON-only endpoint) |
| 413 Content Too Large | Body too big | The payload exceeds your size limit |
The order matters — it mirrors how the server actually processes a request. First it checks the content type (fail → 415), then the size (fail → 413), then it parses (fail → 400), then it validates (fail → 422). A malformed JSON payload is a 400, not a 415: the format was right, the content was unparseable. Each code tells the client which stage rejected them, and that's exactly what they need to know to fix it.
Common Causes
In practice, 400s trace back to a short list of repeat offenders:
- Malformed JSON in the request body — the classic. A trailing comma, an unescaped quote, single quotes instead of double, or a client that concatenated strings into "JSON" instead of using a serializer.
- Missing required fields — the request omits a parameter the endpoint can't function without. (If the field is present but invalid, prefer 422.)
- Invalid query parameters —
?page=bananawhere a number is required, unparseable date formats, or duplicated parameters the server can't reconcile. - Body/Content-Type mismatch — sending a JSON body with
Content-Type: text/plain, or form-encoded data declared asapplication/json. The parser reads the header, tries to decode accordingly, and fails. - Oversized or corrupted cookies — the browser-facing one. When accumulated cookies push the request headers past the server's limit, NGINX responds with its famous "400 Bad Request — Request Header Or Cookie Too Large" page. That's why "clear your cookies" is the stock advice for users stuck on a 400: the browser is sending a broken request on every attempt, and no amount of refreshing fixes it.
- Invalid characters in the URL — unencoded spaces, stray
%signs that aren't valid percent-escapes, or control characters. The server can't parse the request line, so nothing after it matters.
Notice a pattern: the fix for a 400 is always on the client side. Retrying the identical request will fail identically — something about the request must change first. (That's also why smart retry logic never retries 4xx errors, unlike 429 or 5xx.)
Returning 400 Correctly Across Platforms
The status code is only half the job — the other half is a response body specific enough that the client can fix their request. "Invalid request" is a dead end; "expected JSON but the body failed to parse at position 47" is actionable.
Express / Node.js
express.json() throws on malformed bodies, so catch parse errors in your error-handling middleware and keep field checks in the route:
const express = require("express");
const app = express();
app.use(express.json());
app.post("/api/orders", (req, res) => {
// Structural check: the required field is missing entirely → 400
if (!req.body.items) {
return res.status(400).json({
error: "missing_field",
message: "Request body must include an 'items' array.",
});
}
// ... create the order
res.status(201).json({ created: true });
});
// express.json() forwards malformed-body errors here
app.use((err, req, res, next) => {
if (err.type === "entity.parse.failed") {
return res.status(400).json({
error: "malformed_json",
message: "Request body is not valid JSON.",
});
}
next(err);
});The two branches are the whole 400 story in miniature: one for "I couldn't parse it," one for "a required piece is missing entirely." Anything subtler than that — present-but-invalid values — belongs in a 422 branch.
Next.js App Router
In a route handler, request.json() throws on a malformed body, so wrap it and return the 400 yourself:
// app/api/orders/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: "malformed_json", message: "Request body is not valid JSON." },
{ status: 400 },
);
}
if (typeof body !== "object" || body === null || !("items" in body)) {
return NextResponse.json(
{ error: "missing_field", message: "Request body must include 'items'." },
{ status: 400 },
);
}
// ... create the order
return NextResponse.json({ created: true }, { status: 201 });
}An uncaught request.json() failure becomes a 500 — which tells the client you broke when they sent garbage. The try/catch is what keeps the blame where it belongs.
NGINX
NGINX already returns 400 on its own for requests it can't parse — malformed request lines, invalid headers, and the "Request Header Or Cookie Too Large" case (tunable via large_client_header_buffers). To reject requests missing something your backend requires, return one explicitly:
location /api/ {
# Reject API calls that don't declare a version header
if ($http_x_api_version = "") {
return 400 '{"error": "missing_api_version_header"}';
}
proxy_pass http://backend;
}You can also give the default 400 a friendlier face with error_page 400 /errors/400.html; — useful for the cookie-overflow case, where the visitor needs to be told to clear cookies, not shown a bare white error page.
REST API JSON body
For machine consumers, pair the status with a structured body that says what kind of 400 this is and where the problem sits:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "malformed_json",
"message": "Request body is not valid JSON (unexpected token at position 47).",
"documentation": "https://api.example.com/docs/errors#malformed_json"
}A stable machine-readable error code, a human-readable message, and a docs link. Clients branch on the first, developers read the second, and the third saves a support ticket. (If you want a standard shape, RFC 9457's Problem Details format — application/problem+json — gives you exactly these fields with agreed-upon names.)
400 and SEO
Search engines treat a 400 as a hard client error:
- No indexing. Pages returning 400 are not indexed — search engines won't include the URL in results, and an important page stuck on 400 effectively vanishes from search.
- Reduced crawling. Crawlers note the error and may reduce crawl frequency for URLs that consistently return 400, though they'll retry occasionally in case it was transient.
- Watch Search Console. Google Search Console's crawl-error reports are the fastest way to find URLs Googlebot sees as 400 — often malformed internal links or URL-generation bugs you can't reproduce in a browser. Fix these quickly if they affect pages that matter.
One trap specific to 400s: they're frequently caused by the links themselves — a template producing URLs with unencoded characters or mangled query strings. Every internal link pointing at such a URL leaks crawl budget into an error page.
Common Pitfalls
- Using 400 as the only client-error code. When validation errors, auth failures, and parse errors all come back as 400, clients are reduced to string-matching your error messages. Use 401 for missing auth, 403 for permissions, 422 for validation — and let 400 mean what it means.
- Returning 200 with an error body.
200 OK+{"error": "bad request"}breaks every HTTP client's success/failure branching, poisons caches (error responses cached as good ones), and lies to your monitoring. The status line and the body must agree. - A 400 with no explanation. An empty body or a bare "Bad Request" forces the client to guess among a dozen possible causes. Always say what was malformed.
- Returning 500 for unparseable input. If your framework throws on malformed JSON and you don't catch it, the client's mistake surfaces as your server error — waking up your on-call for someone else's trailing comma. Catch parse failures and convert them to 400.
- Blaming the user for a server-side 400. If your own frontend generates requests the backend rejects — a serializer mismatch, an API contract drift after a deploy — users see "Bad Request" for something they can't fix. A 400 spike right after a release is a bug in your client code, not a user problem.
- Forgetting the cookie case in user support. For end users hitting a 400 on a site that worked yesterday, corrupted or oversized cookies are the leading suspect. Clearing cookies for that domain (or the whole browser) resolves it — worth a line in your help docs before "contact support."
Wrapping Up
400 is the most-used and least-precisely-used code in HTTP. The rules of thumb:
- 400 means the request itself is broken — malformed syntax, unparseable body, invalid framing
- Can't parse it → 400. Parsed but invalid → 422. The JSON-linter test settles most cases
- It's also the legitimate generic fallback when no more specific 4xx applies — but reach for the specific code first
- Always ship a body that says what was malformed; a bare 400 is a guessing game
- Catch your framework's parse errors so client garbage surfaces as 400, not 500
- For end users, a persistent 400 usually means oversized or corrupted cookies — clearing them is the fix
For more, see our pages on 400 Bad Request, 422 Unprocessable Entity, and 415 Unsupported Media Type. The neighboring client errors have their own deep-dives: understanding 401 Unauthorized, understanding 403 Forbidden, and understanding 429 Too Many Requests.