400 Bad Request in REST APIs: Causes, Fixes and How to Prevent It
A 400 Bad Request response means the server will not process the request because it sees something wrong on the client side, such as malformed syntax or invalid framing. RFC 9110 section 15.5.1 defines it this way. In a REST API, the usual causes are invalid JSON, a wrong Content-Type, missing or wrongly typed fields, bad query parameters, or headers and cookies that are too large. Resending the same request unchanged will fail again: fix the request, not the server.
This guide explains what the 400 error code in REST API calls means (whether you saw it as a 400 Bad Request API response, a REST API 400 error in your logs, or an "API 400 error" from a client library), how it differs from 401, 403, 404, 409, 415 and 422, what each popular framework returns by default, how to read a 400 API response from curl, the browser, Postman, JavaScript and Python, and what a good error body looks like (RFC 9457 problem details). If you design APIs, the second half shows how to return 400s that people and AI coding agents can fix on the first try.
What response code 400 means
HTTP status codes in the 4xx range mean "the client did something wrong." 400 is the generic one. RFC 9110 says a server uses it when it "cannot or will not process the request due to something that is perceived to be a client error," and gives three examples: malformed request syntax, invalid request message framing, or deceptive request routing. So 400 is the fallback when no more specific 4xx code fits. The HTTP Bad Request status code is the same in every HTTP version (HTTP/1.1, HTTP/2 and HTTP/3 all share RFC 9110's semantics), and the standard reason phrase is "Bad Request", which is why logs show HTTP/1.1 400 Bad Request.
Two practical points follow:
- For API clients: a 400 is not retryable as-is. Read the response body, change the request, then send it again.
- For API designers: use a more specific code when one exists, and always say what was wrong in the body. RFC 9110 section 15.5 says that for any 4xx (except a reply to
HEAD) the server SHOULD send a representation explaining the error and whether it is temporary or permanent.
What "400 error in API" usually looks like
A REST 400 error (or "REST error 400", as some logs phrase it) almost always arrives in one of these shapes, depending on who rejected the request:
| Who rejected it | What you typically see | Clue |
|---|---|---|
| Your API framework | JSON body naming a field ("quantity": ["A valid integer is required."]) |
Your app logs show the request |
| A third-party API | Vendor JSON ("type": "invalid_request_error", "message": ...) |
The vendor's docs list the error type |
| Web server or proxy (nginx, Apache, CDN, load balancer) | Short HTML page, "400 Bad Request" or "Request Header Or Cookie Too Large" | Your app logs nothing at all |
| The HTTP library itself | No response, a client-side exception before sending | Error mentions an invalid URL or header |
That last column matters: if your application has no log line for the request, the 400 came from something in front of it. A 400 Bad Request REST API response from your own code, by contrast, should always leave a trace in your logs.
A short history of status code 400
- May 1996RFC 1945 (HTTP/1.0) defines 400 Bad Request: the request could not be understood due to malformed syntax.
- June 1999RFC 2616 (HTTP/1.1) keeps the same narrow 'malformed syntax' wording.
- June 2007RFC 4918 (WebDAV) introduces 422 Unprocessable Entity for well-formed but semantically wrong content.
- April 2012RFC 6585 adds 428, 429 and 431 Request Header Fields Too Large.
- June 2014RFC 7231 broadens 400 to anything 'perceived to be a client error', not only bad syntax.
- April 2016RFC 7807 defines
application/problem+jsonfor machine-readable API errors. - June 2022RFC 9110 (HTTP Semantics) becomes the current definition and moves 422 Unprocessable Content into core HTTP.
- 2023RFC 9457 replaces RFC 7807 and adds a registry of common problem types.
The 2014 change is why you see 400 used for validation failures today: the definition stopped being only about syntax.
400 vs other 4xx status codes
| Code | Meaning (RFC 9110) | Use it when |
|---|---|---|
| 400 Bad Request | Generic client error | Malformed JSON, unparseable query string, invalid framing |
| 401 Unauthorized | Missing or invalid authentication | No token, expired token, wrong API key |
| 403 Forbidden | Authenticated but not allowed | Valid user without permission for this resource |
| 404 Not Found | Resource does not exist (or is hidden) | Wrong ID or path |
| 405 Method Not Allowed | Method not supported on this resource | DELETE on a read-only endpoint |
| 409 Conflict | Conflicts with the resource's current state | Duplicate unique value, version mismatch |
| 413 Content Too Large | Body exceeds the server limit | Upload or JSON body too big |
| 414 URI Too Long | Request target too long | Huge query string, filters stuffed into the URL |
| 415 Unsupported Media Type | Body format not accepted | Content-Type: text/plain sent to a JSON endpoint |
| 422 Unprocessable Content | Syntax is fine, content is invalid | Valid JSON, but email is not an email address |
| 429 Too Many Requests | Rate limited (RFC 6585) | Too many calls; retry after a delay |
| 431 Request Header Fields Too Large | Headers too big (RFC 6585) | Oversized cookies or tokens |
The 400 vs 422 line is where APIs differ most. A common convention: return 400 when the request can't be parsed, and 422 when it parses but fails validation. RFC 9110's 422 definition fits that split: the server understands the content type and the syntax is correct, but it can't process the instructions. Stripe returns 400 for invalid parameters ("often due to missing a required parameter," per its error docs), while FastAPI defaults to 422. Either is acceptable if you are consistent and document it. Some servers also send 400 where 413 or 431 would be more precise, so a 400 with a huge cookie header is still worth suspecting.
400 vs 401 and 403
Auth problems should not be 400s. If the token is missing, malformed or expired, the right code is 401, and the response should carry a WWW-Authenticate header. If the token is valid but the user lacks permission, it is 403. A 400 that turns out to be an auth problem usually means a header was mangled (for example Authorization: Bearer with an empty token, or a token with a stray newline), so the server couldn't even parse it.
400 vs 404
A 404 says "this resource doesn't exist." A 400 says "I can't make sense of what you asked." GET /orders/9999 for a missing order is 404. GET /orders/abc where IDs must be numeric is arguably 400 (the path can't be parsed into an ID), though many APIs return 404 there too. One practical difference: RFC 9110 lists 404 as heuristically cacheable, and 400 is not, so a CDN may cache a 404 but not a 400 unless you send explicit cache headers.
400 vs 415
If the server rejects the body because of its declared format, 415 is the precise code. RFC 9110 says a 415 response can include an Accept header listing the media types that would have worked. In practice, many stacks return 400 for a missing or wrong Content-Type because the body parser simply sees no JSON. Spring, by contrast, maps HttpMediaTypeNotSupportedException to 415 by default (see the table below).
Common causes of a 400 Bad Request in REST APIs
- Malformed JSON. A trailing comma, single quotes, an unescaped quote inside a string, or a body cut off by the shell.
{"name": "Ada",}is invalid JSON. Unescaped line breaks inside a string are another classic: MDN's 400 example is exactly that. - Wrong or missing
Content-Type. You send JSON but the header saysapplication/x-www-form-urlencoded, or there is no header, so the server can't parse the body. Some servers answer 415, many answer 400. Two well-known traps:curl -ddefaults toapplication/x-www-form-urlencoded(per the curl manual), and Python'srequests.post(url, data=json.dumps(payload))does not setContent-Typeat all (per the Requests quickstart). - Missing required fields or wrong types.
"quantity": "3"(a string) where the schema expects an integer, or a date in the wrong format (11/04/2026instead of ISO 86012026-11-04). - Bad query parameters. Unknown filter names, invalid values (
?limit=abc), or characters that weren't URL-encoded (spaces,&,#,+in a value). - Encoding problems. A body that isn't valid UTF-8, a double-encoded URL (
%2520), or a byte-order mark at the start of a JSON file. - Headers or cookies too large. A browser with years of cookies for a domain, or a giant JWT, can exceed the server's header limit. Many servers and proxies answer with "400 Bad Request: Request Header Or Cookie Too Large" instead of 431. nginx, for example, returns 400 when one header field is larger than a
large_client_header_buffersbuffer (see the limits below). - Invalid framing. A
Content-Lengththat doesn't match the body, or a body sent withGETto a server that rejects it. Express's body parser reports a length mismatch asrequest.size.invalidwith status 400 (body-parser docs). - Wrong top-level JSON shape. Sending an array where the endpoint expects an object (or a bare string). GitHub's REST API answers this with "Body should be a JSON object" and a 400 (GitHub troubleshooting docs).
- Empty or unresolved variables.
{{baseUrl}}orundefinedleft in a URL,"id": null, or a template that never got filled. Postman's own troubleshooting guide calls out empty variables as a frequent cause of failed requests. - Business rules reported as 400. Some APIs use 400 for things like "start date after end date" or "coupon expired." These are not syntax errors, so read the body: the fix is a different value, not different formatting.
Default request limits that trigger 400, 413 or 431
Oversized requests are the cause people overlook, because the same request works from one machine and fails from another (more cookies, a longer token). These are the documented defaults as of November 2026:
nginx: 4 buffers of 8k by default; a single field over one buffer gets 400. Apache: 8,190 bytes per field. Node.js: 16 KiB for all headers, 431 on overflow. Go's net/http allows 1 MB (DefaultMaxHeaderBytes). Source: nginx, Apache httpd and Node.js docs
RFC 6265 says browsers should support at least 50 cookies per domain and at least 4,096 bytes per cookie. Fifty full-size cookies would be roughly 200 KB, far more than an 8 KB header field can carry. That is how a site you have used for years starts answering every request with 400.
How to fix a 400 error: debugging checklist
Work through these in order. Most API 400s are found in the first three steps.
- Read the response body. Good APIs say exactly which field failed. Print it in full:
curl -ishows headers and body. - Validate the JSON. Paste the exact bytes you sent into a JSON validator such as our JSON formatter, or run
echo "$BODY" | jq .. Log the serialized body, not the object you think you serialized. - Check
Content-TypeandAccept. For JSON, sendContent-Type: application/json. - Compare against the API docs. Check required fields, types, enum values, date formats and the URL path version (
/v1vs/v2). - Reproduce with a minimal request. Strip optional fields until it works, then add them back one at a time. A tool like Postman (or one of its free alternatives) makes this quick.
- Check encoding. URL-encode query values (
encodeURIComponentin JavaScript) and make sure nothing is encoded twice. Our URL decoder shows quickly whether a value was encoded twice. - Check header size. In a browser, clear the site's cookies or try a private window. In code, look for duplicated or oversized
AuthorizationandCookieheaders. Paste a suspicious token into a JWT decoder to see how big its claims are. - Look at proxies in between. A CDN, load balancer or API gateway can reject a request before your app sees it. If your app logs nothing, the 400 came from upstream.
- Check for a request ID. Many APIs return one in a header or the error body (the Claude API, for example, sends a
request-idheader and arequest_idfield, per its error docs). Quote it when you contact support.
A quick curl test that rules out client libraries:
curl -i -X POST https://api.example.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"product_id": 42, "quantity": 3}'
Debugging an API 400 with curl
curl is the fastest way to see exactly what goes over the wire. Four flags do most of the work:
# -i: print response headers and body
# -v: also print the request line and request headers curl sent
curl -iv https://api.example.com/v1/orders?limit=10
# --json sets Content-Type and Accept to application/json and sends the data
curl -i --json '{"product_id": 42, "quantity": 3}' https://api.example.com/v1/orders
# Read the body from a file so the shell can't mangle quotes
curl -i --json @order.json https://api.example.com/v1/orders
# --fail-with-body: exit with code 22 on a 4xx/5xx but still print the error body
curl --fail-with-body --json @order.json https://api.example.com/v1/orders
According to the curl manual, --json is a shortcut for --data-binary plus Content-Type: application/json and Accept: application/json headers, and it does not check that your data is valid JSON. --fail-with-body was added in curl 7.76.0. Plain curl exits with 0 even on a 400, so scripts that only check the exit code silently miss API errors.
If you have a working request in the browser or in docs, our curl converter turns a curl command into code for your language, which removes a whole class of hand-translation mistakes.
Debugging in the browser: DevTools Network tab
When a web app shows "Failed to load resource: the server responded with a status of 400 (Bad Request)" in the console, the real explanation is in the Network tab:
- Open DevTools (F12, or Cmd+Option+I on macOS) and go to Network.
- Reproduce the action and click the red request with status 400.
- Headers shows the request URL, method, and the request headers your code actually sent, including
Content-Typeand cookies. - Payload shows the query string parameters and request body as sent. This is where you spot
undefined,[object Object]or a missing field. - Response (or Preview) shows the server's error body.
- Right-click the request and choose Copy > Copy as cURL to replay it in a terminal and edit it until it works.
[object Object] in a payload means an object was concatenated into a string instead of passed through JSON.stringify. That is one of the most common front-end causes of an API 400.
Debugging a 400 Bad Request: Postman
If a request fails in Postman, open the Console from the footer (Cmd+Option+C on macOS, Ctrl+Alt+C on Windows and Linux, per Postman's docs). It logs the request as sent, including headers and resolved variable values, plus the raw server response. Check for:
- Unresolved or empty variables in the final URL or body (
{{token}}that was never set). - Body type. In the Body tab, choose raw and JSON so Postman sends
Content-Type: application/json. Selecting form-data or x-www-form-urlencoded by mistake is a classic 400. - Auto-added headers. The Headers tab can show hidden headers Postman adds automatically. A duplicated
Content-TypeorAuthorizationheader can trigger a 400.
Why does my request work in Postman but return 400 in code?
This one also appears in the FAQ below, but the short version: compare the two raw requests side by side. Use Postman's Code snippet view to see the exact request, and log the outgoing request in your code (an HTTP debugging proxy, curl -v, or your library's debug logging). The difference is almost always a header, a serialization step, or URL encoding.
Fixing 400 Bad Request in Chrome (as a user)
If you see "400 Bad Request" on a normal website rather than in your own API, it is usually a corrupted or oversized cookie or a mistyped URL. Check the URL, then clear cookies for that site (in Chrome: the site settings icon in the address bar, then Cookies and site data). Try a private window to confirm.
If a private window works and a normal one doesn't, cookies or a browser extension are the cause. If both fail, the problem is the URL itself or the site.
How different clients report an API 400 response
Every HTTP client surfaces a 400 differently, and some hide the error body by default. That's why "API request failed with status 400" messages are often vague. Here is how to get at the body in the most common clients:
| Client | What a 400 looks like | Does it throw? | Where the error body is |
|---|---|---|---|
fetch() (browser, Node, Deno, Bun) |
response.status === 400, response.ok === false |
No (MDN) | await response.json() or .text() |
| Axios | "Request failed with status code 400" | Yes, by default for any non-2xx (Axios docs) | error.response.data |
Python requests |
<Response [400]> |
Only if you call raise_for_status() (docs) |
r.text or r.json() |
Python urllib |
urllib.error.HTTPError: HTTP Error 400: Bad Request |
Yes (docs) | e.fp.read() on the HTTPError |
| curl | HTTP/1.1 400 Bad Request with -i |
Exit code 0 unless --fail or --fail-with-body |
Printed to stdout |
Java HttpURLConnection |
"Server returned HTTP response code: 400" | Yes, from getInputStream() |
getErrorStream() |
JavaScript: fetch and Axios
fetch() only rejects on network failures, so you must check response.ok yourself and read the body:
async function createOrder(order) {
const res = await fetch('https://api.example.com/v1/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order), // never send the raw object
});
if (!res.ok) {
const text = await res.text();
let problem;
try { problem = JSON.parse(text); } catch { problem = { detail: text }; }
const err = new Error(`API error ${res.status}: ${problem.title ?? problem.detail ?? res.statusText}`);
err.status = res.status;
err.problem = problem;
throw err;
}
return res.json();
}
With Axios, the message "Request failed with status code 400" is generic. The useful part is on error.response:
try {
await axios.post('/v1/orders', order);
} catch (error) {
if (error.response) {
// The server answered with a non-2xx status
console.error(error.response.status, error.response.data);
} else if (error.request) {
// Sent, but no response arrived (network, CORS, timeout)
console.error('No response', error.message);
} else {
console.error('Request setup failed', error.message);
}
}
Python: requests and urllib
import requests
r = requests.post(
"https://api.example.com/v1/orders",
json={"product_id": 42, "quantity": 3}, # json= sets Content-Type for you
timeout=10,
)
if r.status_code == 400:
print("Bad request:", r.text) # read the body before raising
r.raise_for_status()
Use json= instead of data=json.dumps(...). The Requests docs note that data= with a string will not set Content-Type, while json= encodes the body and sets the header.
With the standard library, the 400 arrives as an exception and the body is on it:
import json, urllib.request, urllib.error
req = urllib.request.Request(
"https://api.example.com/v1/orders",
data=json.dumps({"product_id": 42, "quantity": 3}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
print(resp.status, resp.read().decode())
except urllib.error.HTTPError as e:
print(e.code, e.reason) # 400 Bad Request
print(e.fp.read().decode()) # the API's explanation
Should you retry a 400?
No, not automatically. A 400 means the request itself is wrong, so the same bytes fail the same way. Retry logic with exponential backoff belongs on 429, 503 and some network errors. The Claude API's docs, for example, say its official SDKs automatically retry transient failures (connection errors, rate limits and 5xx server errors); a 400 is not on that list. The exception: if a 400 came from a proxy because of a stale cookie or token, refreshing that credential and sending a new request is fine.
What popular frameworks and APIs return by default
If you build an API, your framework already decides much of this for you. These defaults are from each project's documentation as of November 2026:
| Framework or API | Malformed body | Validation failure | Error body format |
|---|---|---|---|
Express + express.json() |
400 (entity.parse.failed) |
Whatever your code returns | Your choice |
| FastAPI | 422 (json_invalid, raised as RequestValidationError in routing.py) |
422 by default (docs) | {"detail": [{"loc", "msg", "type"}]} |
| Django REST framework | 400 (ParseError) |
400 (ValidationError) (docs) |
{"field": ["message"]} |
| Spring MVC / Spring Boot | 400 (HttpMessageNotReadableException) |
400 (MethodArgumentNotValidException) (javadoc) |
ProblemDetail (RFC 9457) when enabled |
ASP.NET Core with [ApiController] |
400 | 400, automatic (docs) | ValidationProblemDetails |
| GitHub REST API | 400 ("Problems parsing JSON") | 422 ("Validation Failed") (docs) | message plus errors[].code |
| Stripe API | 400 | 400 (invalid_request_error) (docs) |
error.type, code, message, param, doc_url |
| Claude API | 400 | 400 (invalid_request_error) (docs) |
error.type, error.message, request_id |
Two details worth knowing from those docs. Spring maps an unsupported Content-Type (HttpMediaTypeNotSupportedException) to 415, not 400. And Stripe's param field names the parameter that failed, which is exactly the kind of pointer a good 400 body should include.
Common third-party API 400 messages and what they mean
- "Problems parsing JSON" (GitHub): the body isn't valid JSON. Validate the exact bytes.
- "Body should be a JSON object" (GitHub): you sent an array or a string at the top level.
invalid_request_error(Stripe, Claude API): a parameter is missing, has the wrong type, or isn't allowed. Readmessage(andparamon Stripe).{"detail": "Bad Request"}: a generic message, often from a Python framework's default handler. The server knows more than it says; check its logs or docs.- "API error: 400 upstream rejected the request": a gateway or proxy is passing along a 400 from the service behind it. Look for that service's own error body or logs.
Return useful 400 responses: RFC 9457 problem details
RFC 9457 (which replaced RFC 7807) defines a standard JSON error format with the media type application/problem+json. Its members are type, title, status, detail and instance, and you can add your own fields, such as a list of field errors:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid.",
"status": 400,
"detail": "2 fields failed validation.",
"instance": "/v1/orders",
"errors": [
{ "pointer": "#/quantity", "detail": "must be an integer greater than 0" },
{ "pointer": "#/product_id", "detail": "is required" }
]
}
Clients can handle every error the same way, and humans can read detail without opening your source code.
What each member means, per the RFC:
| Member | Meaning | Tips |
|---|---|---|
type |
URI that identifies the problem type; the primary identifier clients match on | Use an absolute URI you control. If omitted, it defaults to about:blank (no meaning beyond the status code). Clients should not fetch it automatically. |
title |
Short, human-readable summary of the problem type | Keep it the same for every occurrence of that type (except for translation). |
status |
The HTTP status code, as a number | Advisory: it must match the real status line. |
detail |
Human-readable explanation of this occurrence | Say how to fix it. Clients should not parse it. |
instance |
URI for this specific occurrence | A request path or an ID you can look up in logs. |
| extensions | Any extra members, like errors |
Clients must ignore extensions they don't recognize. |
The RFC's own validation example uses an errors array where each entry has a detail and a pointer (a JSON Pointer into the request body, such as #/age). That is a good pattern to copy. Note that the RFC's example uses status 422: the format works the same with 400.
Write 400 messages that people and AI coding agents can fix
More API calls are now written and debugged by AI coding agents, which read your error body and decide what to change. A vague 400 ("Bad Request", "Invalid input") gives them, and humans, nothing to act on, so they guess or give up. A good 400 body answers four questions:
- Which field? A JSON Pointer (
#/items/0/quantity) or parameter name. - What rule? "must be an integer between 1 and 100", not "invalid".
- What did you get? Echo the type or value back when it's safe: "got string
"3"". - Where to read more? A
typeURI or doc link, like Stripe'sdoc_url.
Compare:
{ "error": "Bad Request" }
{
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid.",
"status": 400,
"detail": "quantity must be an integer between 1 and 100 (got string \"3\").",
"errors": [{ "pointer": "#/quantity", "detail": "expected integer, got string" }]
}
The second one gets fixed on the first try. Keep error messages stable too: changing wording breaks clients that (against the RFC's advice) match on text, so give each problem a stable type or code and let the wording evolve.
Prevent 400s: validate on the server
Validate every request at the edge of your API, before business logic runs, and turn failures into a clear 400 (or 422). Here is an Express example with Zod:
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json({ limit: '100kb' }));
const Order = z.object({
product_id: z.number().int().positive(),
quantity: z.number().int().min(1).max(100),
});
app.post('/v1/orders', (req, res) => {
const result = Order.safeParse(req.body);
if (!result.success) {
return res.status(400).type('application/problem+json').json({
type: 'https://api.example.com/problems/validation-error',
title: 'Your request is not valid.',
status: 400,
errors: result.error.issues.map((i) => ({
pointer: '#/' + i.path.join('/'),
detail: i.message,
})),
});
}
res.status(201).json({ id: 1, ...result.data });
});
// Malformed JSON: express.json() throws before the route runs
app.use((err, req, res, next) => {
if (err.type === 'entity.parse.failed') {
return res.status(400).type('application/problem+json').json({
title: 'Malformed JSON body.',
status: 400,
});
}
if (err.type === 'entity.too.large') {
return res.status(413).type('application/problem+json').json({
title: 'Request body too large.',
status: 413,
});
}
next(err);
});
app.listen(3000);
The error types come from the body-parser docs: entity.parse.failed (400) for bad JSON and entity.too.large (413) when the body exceeds limit, which defaults to 100 KB.
FastAPI: return 400 instead of the default 422
FastAPI returns 422 for request validation errors. If your API's convention is 400, override the RequestValidationError handler, as the FastAPI docs show:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=400,
media_type="application/problem+json",
content={
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid.",
"status": 400,
"errors": [
{"pointer": "#/" + "/".join(str(p) for p in e["loc"][1:]), "detail": e["msg"]}
for e in exc.errors()
],
},
)
Build the body from exc.errors() rather than str(exc): the FastAPI docs warn that the string form can leak internal details.
Spring Boot: ProblemDetail out of the box
Current Spring Framework versions include a ProblemDetail class that renders as application/problem+json. In Spring Boot, set spring.mvc.problemdetails.enabled=true and the built-in exceptions (bad JSON, failed @Valid, missing parameters) return RFC 9457 bodies with status 400, as described in the Spring reference. For your own exceptions, return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "...") from an @ExceptionHandler.
ASP.NET Core: automatic 400s
With the [ApiController] attribute, model validation errors automatically produce a 400 with a ValidationProblemDetails body ("One or more validation errors occurred."), so you don't need if (!ModelState.IsValid) checks in each action. Microsoft's web API docs recommend calling ValidationProblem() instead of BadRequest() in your own code so custom and automatic errors look the same.
Client-side prevention
On the client side, prevention is mostly discipline:
- Build bodies with
JSON.stringify(or your language's serializer), never by joining strings. - Build query strings with
URLSearchParamsinstead of concatenating them. - Generate types or clients from the API's OpenAPI spec so wrong field names fail at compile time. Our JSON to TypeScript converter is a quick start when there's no spec.
- Log the outgoing request (minus secrets) when a call fails, so you can see what was really sent.
- Validate user input in the UI with the same rules as the API, so most bad requests never leave the browser.
- Keep cookies small: store a session ID in the cookie and the data on the server.
If you are building against third-party APIs, our list of free APIs for developers and the API tools directory cover testing and mocking tools that help catch these issues early. For writing docs that prevent 400s in the first place (examples for every field, required vs optional, formats), see REST API documentation best practices. And when you need to look up a less common code quickly, keep the HTTP status codes reference handy.
Test your 400 paths
Error paths are code too. For each endpoint, add tests that send: invalid JSON, no Content-Type, each required field missing, each field with the wrong type, out-of-range values, unknown query parameters, and an oversized body. Assert both the status code and the body shape. Contract tests generated from an OpenAPI spec cover much of this automatically, and the testing tools directory lists options.
FAQ
What is response code 400?
It is the HTTP status for "Bad Request": the server rejected the request because of a client-side problem such as invalid syntax, an unreadable body or bad parameters. The fix is to change the request. Retrying it unchanged will fail again.
What is 400 error in API terms, exactly?
A 400 error in an API means the API received your call but rejected its contents: the JSON didn't parse, a required field was missing, a value had the wrong type, or a header was invalid. The response body usually names the problem. The 400 status code in REST API design is the default answer for "fix your request."
Is a 400 error my fault or the server's?
By definition the server thinks the client made the mistake. Sometimes the server is wrong, for example when a proxy rejects valid headers or when an API's docs are out of date. Still, start debugging on the client.
Should an API return 400 or 422 for validation errors?
Both are used. A common split is 400 for requests that can't be parsed and 422 for well-formed requests that fail validation rules. Pick one convention, document it, and put field-level details in the body.
How do I fix "400 Bad Request: Request Header Or Cookie Too Large"?
Clear the cookies for that site, or try a private window. If it's your own app, shrink the cookies or tokens you set, or raise the header size limit on your web server or proxy (for nginx, large_client_header_buffers; for Apache, LimitRequestFieldSize).
Why does my request work in Postman but return 400 in code?
Usually the code sends something different: a missing Content-Type, an object that was never serialized, a double-encoded URL, or extra headers added by a library. Log the raw outgoing request and compare it with Postman's.
How do I fix a 400 Bad Request error in Postman?
Open the Postman Console to see the request as sent, then check that every variable resolved, that the body type is raw with JSON selected, and that no header is duplicated. Compare the request with the API docs field by field.
Why does an API request fail with status 400 only sometimes?
Look for something that varies: the size of cookies or tokens (more cookies, longer JWTs), user input with special characters that isn't URL-encoded, optional fields that are sometimes null, or a load balancer routing to servers with different limits or versions.
Is "400 Bad Request" the same as "HTTP 400" and "bad request code"?
Yes. HTTP 400, status code 400, the bad request code and "400 Bad Request" all refer to the same status. "Bad Request" is the standard reason phrase that follows the number.
Should my client retry on a 400?
No. Retries with backoff are for 429, 503 and network errors. Change the request first; a 400 only goes away when the request does.
What does "Request failed with status code 400" mean in Axios?
Axios rejects every non-2xx response by default and uses that generic message. The API's real explanation is in error.response.data; log it to see which field or header was wrong.