Published Sep 16, 2025 ⦁ Updated Nov 4, 2026 ⦁ 7 min read

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

  1. May 1996RFC 1945 (HTTP/1.0) defines 400 Bad Request: the request could not be understood due to malformed syntax.
  2. June 1999RFC 2616 (HTTP/1.1) keeps the same narrow 'malformed syntax' wording.
  3. June 2007RFC 4918 (WebDAV) introduces 422 Unprocessable Entity for well-formed but semantically wrong content.
  4. April 2012RFC 6585 adds 428, 429 and 431 Request Header Fields Too Large.
  5. June 2014RFC 7231 broadens 400 to anything 'perceived to be a client error', not only bad syntax.
  6. April 2016RFC 7807 defines application/problem+json for machine-readable API errors.
  7. June 2022RFC 9110 (HTTP Semantics) becomes the current definition and moves 422 Unprocessable Content into core HTTP.
  8. 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).

Best for simple APIs and teams that want one rule: 400 for every client-side input error
Matches Stripe, Django REST framework and ASP.NET Core defaults. Clients only need to handle one code; put the detail in the body.
Best for APIs with rich validation and typed clients: 400 for unparseable, 422 for invalid
Matches GitHub's REST API and FastAPI. Clients can tell 'fix your serializer' apart from 'fix this field value'.
Best for every API, whichever of the two above you pick: Specific codes first (401/403/404/409/413/415/429)
A 400 should be the fallback, never a replacement for a code that carries its own meaning and retry rules.

Common causes of a 400 Bad Request in REST APIs

  1. 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.
  2. Wrong or missing Content-Type. You send JSON but the header says application/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 -d defaults to application/x-www-form-urlencoded (per the curl manual), and Python's requests.post(url, data=json.dumps(payload)) does not set Content-Type at all (per the Requests quickstart).
  3. 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/2026 instead of ISO 8601 2026-11-04).
  4. Bad query parameters. Unknown filter names, invalid values (?limit=abc), or characters that weren't URL-encoded (spaces, &, #, + in a value).
  5. 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.
  6. 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_buffers buffer (see the limits below).
  7. Invalid framing. A Content-Length that doesn't match the body, or a body sent with GET to a server that rejects it. Express's body parser reports a length mismatch as request.size.invalid with status 400 (body-parser docs).
  8. 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).
  9. Empty or unresolved variables. {{baseUrl}} or undefined left 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.
  10. 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:

8 KB
nginx: max size of one request header field before a 400
8,190 B
Apache httpd 2.4: default LimitRequestFieldSize
16 KiB
Node.js: default max size of all request headers (431 on overflow)
100 KB
Express express.json() default body limit (413 above it)
1 MB
nginx default client_max_body_size (413 above it)
4,096 B
Minimum cookie size browsers SHOULD support, per cookie
Default request header limits (KB)
Apache, per header field 8 KB
nginx, per header field 8 KB
Node.js, all headers 16 KB
nginx, all header buffers 32 KB

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.

  1. Read the response body. Good APIs say exactly which field failed. Print it in full: curl -i shows headers and body.
  2. 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.
  3. Check Content-Type and Accept. For JSON, send Content-Type: application/json.
  4. Compare against the API docs. Check required fields, types, enum values, date formats and the URL path version (/v1 vs /v2).
  5. 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.
  6. Check encoding. URL-encode query values (encodeURIComponent in JavaScript) and make sure nothing is encoded twice. Our URL decoder shows quickly whether a value was encoded twice.
  7. Check header size. In a browser, clear the site's cookies or try a private window. In code, look for duplicated or oversized Authorization and Cookie headers. Paste a suspicious token into a JWT decoder to see how big its claims are.
  8. 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.
  9. Check for a request ID. Many APIs return one in a header or the error body (the Claude API, for example, sends a request-id header and a request_id field, 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:

  1. Open DevTools (F12, or Cmd+Option+I on macOS) and go to Network.
  2. Reproduce the action and click the red request with status 400.
  3. Headers shows the request URL, method, and the request headers your code actually sent, including Content-Type and cookies.
  4. Payload shows the query string parameters and request body as sent. This is where you spot undefined, [object Object] or a missing field.
  5. Response (or Preview) shows the server's error body.
  6. 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-Type or Authorization header 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.

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. Read message (and param on 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:

  1. Which field? A JSON Pointer (#/items/0/quantity) or parameter name.
  2. What rule? "must be an integer between 1 and 100", not "invalid".
  3. What did you get? Echo the type or value back when it's safe: "got string "3"".
  4. Where to read more? A type URI or doc link, like Stripe's doc_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 URLSearchParams instead 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.

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.