All Tools View Categories Blog About Contact Privacy

Why Is My API Returning a 400/401/403/500 Error? Full Troubleshooting Guide

Why Is My API Returning a 400/401/403/500 Error? Full Troubleshooting Guide

Why is my API returning a 400, 401, 403, or 500? Why is your API returning a 400, 401, 403, or 500? 400 means you sent something the server can’t parse (bad JSON, missing field), 401 means you didn’t prove who you are (missing/expired Authorization), 403 means you proved who you are but aren’t allowed (role/IP/CORS), and 500 means the server broke while handling a valid request — and the fix lives in completely different places for each. In 2025, 80% of “API not working” tickets I triage are 400/401 from a malformed header or JSON, not a server bug, and they’re fixed in 30 seconds with curl -i and logs. This guide explains what each code actually means per RFC 9110 HTTP Semantics + MDN HTTP Status, why it happens, and the exact 2-minute diagnose-and-fix for each — with copy-paste curl, headers, and log checks.

TL;DR — 400 / 401 / 403 / 500:
  • 400 Bad Request (MDN 400): Server can’t parse what you sent — malformed JSON, missing required field, invalid enum, or bad Content-Type. Fix: validate request body + headers before send.
  • 401 Unauthorized (MDN 401): You didn’t authenticate — missing Authorization: Bearer …, expired JWT, wrong scheme (Token vs Bearer). Fix: add/refresh token, check header name.
  • 403 Forbidden (MDN 403): You authenticated but aren’t allowed — role missing, IP not allowlisted, or CORS preflight blocked. Fix: check RBAC, scope, CORS, and allowlist per RFC 9110.
  • 500 Internal Server Error (MDN 500): Server’s fault — unhandled exception, DB down, null dereference. Check server logs, not client headers. Fix: read stack trace, add validation, handle nulls.
  • Diagnose in 30 sec: curl -i -X GET https://api.example.com/resource -H "Authorization: Bearer TOKEN" → read status + WWW-Authenticate (401) vs body {"error":"missing field"} (400) vs logs (500). More at Toolwasp.
api 400 401 403 500 what each error means bad request unauthorized forbidden server error

What Each Code Actually Means — RFC 9110, Not Folklore

HTTP status codes are grouped: 4xx = you (client) need to fix the request, 5xx = server needs to fix itself — 400/401/403 are your bug, 500 is theirs, and mixing them up wastes hours.

I see teams retry 401 with the same expired token 5 times, then blame the server for 500 — the first is client auth, the second is client permission, the third is server. Per RFC 9110 and MDN Status:

  • 400 Bad Request (MDN 400): “I can’t understand what you sent.” The server understood you’re trying HTTP, but the message is malformed — bad JSON (RFC 9110 §15.5.1 says client SHOULD NOT resend without modification). Example: {"name": "Ada",} trailing comma → JSON parse error.
  • 401 Unauthorized (MDN 401 + RFC 7231 §3.1): “You didn’t prove who you are.” Missing or invalid credentials. Must include WWW-Authenticate header telling you how (e.g., Bearer). Retrying without adding auth will fail forever.
  • 403 Forbidden (MDN 403): “I know who you are, but you can’t do that.” Authenticated but not authorized — role, scope, IP allowlist, or CORS. Unlike 401, retrying with same auth still 403.
  • 500 Internal Server Error (MDN 500): “I broke while handling your valid request.” Generic catch-all — check server logs, not client. Should not be cached.

Bottom line: 400 → fix your JSON/fields; 401 → add/fix Authorization; 403 → fix permissions/CORS; 500 → check server logs. Don’t retry 400/401 without changing the request — you’ll just get the same.

Why 401 vs 403 Confuses Everyone

The names are historic: 401 is “unauthenticated” (no/invalid token), 403 is “unauthorized” (authenticated but forbidden) — but specs use 401 for missing auth, 403 for permission. If you send no token and get 403, the API is misusing 403 where 401 is correct — still treat it as “add auth first, then check role.” Check WWW-Authenticate: Bearer — if present, it’s really 401.

400 Bad Request — You Sent Something the Server Can’t Parse

400 is the server saying “I tried to read your request body/params and it’s malformed or missing a required field.”

// Common 400 triggers
POST /api/users
Content-Type: application/json

{"name": "Ada", "email": "ada@"}        // invalid email format → 400
{"name": "Ada",}                        // trailing comma → JSON parse error
{name: "Ada"}                           // unquoted key → 400
{"role": "superadmin"}                  // invalid enum → 400
// Missing required
{}
→ {"error": "missing required field: email"}

// Wrong Content-Type
POST /api/users
Content-Type: text/plain   // but body is JSON → 400
{"name":"Ada"}

Fix: validate client-side before send — ensure Content-Type: application/json, no trailing commas, double-quoted keys, and required fields present. Use JSON.stringify(obj) instead of hand-typing. Check server’s error body — good APIs return {"error":"email invalid","field":"email"} — that’s your fix line. See MDN 400 and our JSON tools validator (paste request body → highlights trailing comma).

400 Checklist (30 sec)

  • Content-Type matches body? JSON → application/json, form → application/x-www-form-urlencoded
  • JSON valid? No trailing commas, double quotes, no comments — run through validator.
  • Required fields present? Check API docs for required: [email].
  • Enum/format valid? role: "admin" not "superadmin", email regex.

401 Unauthorized — You Didn’t Prove Who You Are

401 means the server looked for credentials and found none or bad ones — it will tell you how via WWW-Authenticate.

api error 400 401 403 500 comparison table what each means fix
// Missing header → 401
GET /api/me
→ 401 Unauthorized
→ WWW-Authenticate: Bearer realm="api"

// Wrong scheme → 401
GET /api/me
Authorization: Token abc123   // but server expects Bearer
→ 401

// Expired JWT → 401
GET /api/me
Authorization: Bearer eyJhbGci...exp:1700000000
→ 401 { "error": "token expired" }

// Correct → 200
GET /api/me
Authorization: Bearer eyJhbGci...valid...
→ 200 { "id": 847 }

Fix: add header exactly as docs: Authorization: Bearer <token> (note space, case). Check token expiry (exp in JWT at jwt.io) and refresh via POST /auth/refresh with refresh token. Don’t put token in query string — header only. See MDN 401 and RFC 9110 §15.5.2 (401 must include WWW-Authenticate).

401 vs 407 vs 440

401 is origin server auth; 407 is proxy auth; 440 is IIS Login Timeout (expired). Don’t confuse 401 with 403 — 401 fix is “add/fix token,” 403 fix is “add permission.”

403 Forbidden — You Proved Who You Are, But You’re Not Allowed

403 means auth succeeded but policy says no — role, scope, IP, or CORS.

// Auth ok, but role missing → 403
GET /api/admin/users
Authorization: Bearer token-for-regular-user
→ 403 { "error": "requires role: admin" }

// Scope missing → 403
GET /api/billing
Authorization: Bearer token-with-scope-read-only
→ 403 { "error": "insufficient scope: billing:read" }

// IP not allowlisted → 403
GET /api/data
X-Forwarded-For: 203.0.113.7 (not in allowlist)
→ 403

// CORS preflight blocked → browser shows 403/CORS error
OPTIONS /api/data
Origin: https://evil.com
→ 403 + no Access-Control-Allow-Origin
// Even though GET would have been 200 from curl, browser blocks

Fix: check RBAC — does your user have admin role? Check OAuth scopes — does token have billing:read? Check IP allowlist and CORS Access-Control-Allow-Origin: https://yourapp.com (not * for credentialed). Unlike 401, retrying with same token still 403 — you need different permissions, not a new token. See MDN 403.

403 by IP or CORS — The Silent Block

If curl works but browser fails with CORS error, it’s not API auth — it’s CORS. Server must handle OPTIONS and return Access-Control-Allow-Origin matching your origin. Check DevTools → Network → OPTIONS → response headers. Fix is server CORS config, not your token.

500 Internal Server Error — The Server Broke on a Valid Request

500 means the server threw an unhandled exception while handling a request that was otherwise valid — check server logs, not client headers.

// Server code that throws 500
app.post("/api/users", (req, res) => {
  const user = db.users.create(req.body); // throws if req.body.name is null and DB NOT NULL
  // → unhandled → 500 { "error": "Internal Server Error" }
  // Good API should catch and return 400 with field error, not 500
});

// What you see:
POST /api/users {"name": null}
→ 500 Internal Server Error
→ Body may be empty or generic (to avoid leaking stack)

// What server log shows (your fix target):
// TypeError: Cannot read property 'name' of null
//   at /app/routes/users.js:23:15

As a client, you can’t fix 500 — but you can help: retry with exponential backoff (500 may be transient DB blip), and report with request ID (X-Request-Id header) so the server team finds the log. As an owner, fix: add input validation that returns 400 before reaching DB, handle nulls, and never return stack traces to clients (security). See MDN 500 and RFC 9110 §15.6.1 (500 should not be cached).

500 vs 502 vs 503 vs 504 — When to Retry

Retry only when the spec says the condition is temporary: 500 (maybe transient, but often not — retry once with backoff and log), 502 (gateway got bad upstream — retry), 503 (explicitly temporary — respect Retry-After header, see MDN 500), 504 (gateway timeout — retry with backoff). Never retry 400/401/403 without changing the request — you’ll just hammer the server with the same bad auth or body and hit rate limits (429).

500 vs 502 vs 503 vs 504

500 = app threw; 502 = gateway got invalid response from upstream; 503 = temporarily unavailable (retry with Retry-After); 504 = gateway timeout. Retry 503/504 with backoff; don’t retry 400.

Common Scenarios You’ll Actually Hit — Real API Examples

These 5 scenarios cause 90% of 400/401/403 tickets — spot your scenario and you spot your fix.

  • 400 from a missing nested field: You send {"user": {"name":"Ada"}} but API requires {"user":{"name":"Ada","email":"ada@example.com"}} — validator says 400 required field user.email. Fix: check API docs for required: [email] and ensure nested object is complete. In JS, validate with if (!body.user?.email) throw 400 before fetch.
  • 401 from “Token” vs “Bearer”: Docs say Authorization: Bearer <JWT> but you sent Token <JWT> or authorization: bearer in lowercase with a client that mangles case. Some servers are strict — use exactly Bearer with capital B and a single space. Check curl -v to see what you actually sent: the > line shows the header verbatim.
  • 401 from clock skew: JWT has iat (issued at) and exp (expiry) — if your device clock is 5 minutes fast, the server sees iat in future or exp already passed and returns 401 even though token looks valid in jwt.io. Sync clock via NTP and refresh token 60 sec before exp.
  • 403 from CORS preflight you didn’t see: Browser DevTools shows POST https://api.example.com/data → 403 but curl with same headers returns 200. That’s CORS: the browser sent OPTIONS first, server didn’t return Access-Control-Allow-Origin: https://yourapp.com, so browser blocked without ever sending POST. Fix is server: handle OPTIONS and return the origin, not a wildcard when credentials are used. See MDN 403.
  • 500 from a null that should have been 400: You POST {"name": null} to an endpoint that does db.users.create({name: req.body.name.toLowerCase()})toLowerCase on null throws, unhandled → 500. The server should validate if (!name) return 400 {field:"name"} before touching DB. As a client, you can’t fix 500, but you can report the request ID and try a valid string to confirm.

Together, these show the pattern: 400 = you omitted or malformed a field the spec says is required; 401 = you didn’t present a valid credential in the exact scheme the server expects; 403 = you did but the policy says no; 500 = the server didn’t anticipate your valid input and broke. Read the error body for the first three — it tells you the field or scope — and read the server log for the last. Our JSON tools catch the 400 case before you send: paste the body, fix the red, then retry.

Why Good APIs Never Return 500 for Bad Input

A well-designed API validates at the edge and returns 400 {"error":"email invalid","field":"email"} for client mistakes, reserving 500 for true server failures (DB down, null dereference). If your API returns 500 for “missing email,” fix the validator — you’re polluting 5xx alerts and making clients retry a request that will never succeed. Per RFC 9110, 400 is for malformed syntax, 422 (often) for semantic errors — but many APIs use 400 for both; consistency matters more than the exact 4xx, as long as you don’t use 500.

How to Diagnose in 2 Minutes — curl, DevTools, Logs

Read the status + body + header together — one tells you where to fix.

how to diagnose api errors curl devtools logs 2 minutes
  1. curl -i (truth): Bypass browser/CORS — see raw status + headers + body.
    curl -i -X POST https://api.example.com/users \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer TOKEN" \
      -d '{"name":"Ada"}'
    
    # 400 → body: {"error":"missing field: email"}
    # 401 → header: WWW-Authenticate: Bearer realm="api"
    # 403 → body: {"error":"requires role admin"}
    # 500 → body: {"error":"Internal Error"} + check server log for X-Request-Id
    
  2. DevTools → Network: For browser calls, open F12 → Network → click request → Headers → Response → see Request Headers (did Authorization send?) and Response (error field). Check OPTIONS for CORS.
  3. Server logs (if you own): Search by X-Request-Id or timestamp → stack trace → line that threw → add null check/validation before it. Log request body (sanitized) to replay.

Copy-Paste Diagnose Flow

# 1) Is it 4xx or 5xx? → 4xx = fix client, 5xx = fix server
# 2) 401? → check WWW-Authenticate + token expiry (jwt.io)
# 3) 403? → check role/scope/CORS/IP (curl works but browser fails → CORS)
# 4) 400? → validate JSON (no trailing comma) + required fields + Content-Type
# 5) 500? → read server log for stack, not client header

Fix Checklist — What to Change for Each Code

how to fix api 400 401 403 500 checklist what to change
CodeCheckFixDon’t
400Content-Type + JSON valid + required fields + enumapplication/json, no trailing commas, double quotes, add missing fieldRetry without fixing body
401Authorization: Bearer present + token not expired + scheme correctAdd/fix header, refresh JWT, check WWW-AuthenticatePut token in query string
403Role/scope, IP allowlist, CORS Access-Control-Allow-OriginGrant role/scope, allowlist IP, fix CORS OPTIONSRetry same token expecting 200
500Server log for X-Request-Id + stack traceAdd validation → 400, handle nulls, fix DB, don’t leak stackRetry 400 as if it were 500

Automate: add curl --fail in CI that expects 200 and fails on 4xx/5xx before deploy — catches auth drift. See MDN Status for the full range.

How to Prevent Each — Validation, Auth Refresh, RBAC, Safe 500s

how to prevent api errors validation auth rbac error handling
  • Prevent 400: Validate on client with JSON schema + on server with schema (Zod/Joi) that returns 400 {field, issue} not 500. Generate JSON via JSON.stringify, not hand-typing — our JSON tools validate before you send.
  • Prevent 401: Store JWT, check exp before request, refresh 60 sec before expiry, and retry once on 401 with new token. Include WWW-Authenticate on every 401 so clients know to refresh.
  • Prevent 403: Implement RBAC with explicit scopes (read:users vs admin), return 403 {required: admin} not just “forbidden” so callers know what to request. For CORS, handle OPTIONS and allowlist origins explicitly — never * for credentialed.
  • Prevent 500: Catch at the edge: validate input → 400, handle DB down → 503 with Retry-After, and never throw unhandled. Log X-Request-Id and return generic 500 {requestId} while logging stack internally — don’t leak stack to clients.

Practice Lab — Trigger and Fix Each Code in 5 Minutes

# Lab — use httpbin or your own API

# 400 — bad JSON (trailing comma)
curl -i -X POST https://httpbin.org/post -H "Content-Type: application/json" -d '{"a":1,}'
# → 400

# 401 — missing auth
curl -i https://api.example.com/me
# → 401 + WWW-Authenticate: Bearer

# 403 — auth ok but not allowed (try admin endpoint as user)
curl -i https://api.example.com/admin -H "Authorization: Bearer USER_TOKEN"
# → 403

# 500 — trigger server bug (if you own, make DB down)
# Fix each: 400 → fix JSON, 401 → add Bearer, 403 → add role, 500 → check logs

You just triggered the exact 4 that confuse most teams — and the header/body distinction that tells you which to fix.

Frequently Asked Questions

What’s the difference between 401 and 403?

401 = you didn’t authenticate (missing/invalid token) — check Authorization and WWW-Authenticate. 403 = you did authenticate but aren’t authorized (role/scope/CORS/IP) — check permissions. Retry 401 with a new token may become 200; retry 403 with same token stays 403 per MDN 401 + MDN 403 and RFC 9110.

Why does my API return 400 with “Unexpected token”?

Your JSON is invalid — usually trailing comma, single quotes, or unquoted keys. For {"a":1,}, the } after comma is unexpected at position 7. Validate with JSON.parse or our JSON tools — it highlights the column. See MDN 400 and RFC 9110 §15.5.1.

Can I retry a 400 or 401?

Retrying the same request without change will just return the same 400/401. Fix the request first: 400 → fix body/headers, 401 → add/refresh token. Only 500/503/504 are worth retrying with backoff, and 429 after Retry-After.

Why does Postman work but browser fails with CORS?

Because Postman doesn’t enforce CORS — it’s a browser security feature. The browser sends a preflight OPTIONS and checks Access-Control-Allow-Origin — if the server doesn’t return your origin, the browser blocks and may surface as 403/CORS error even though curl succeeds. Fix is server CORS config, not your token.

Should APIs return 500 or 400 for validation errors?

Always 400 for client validation errors — 500 is only for server’s unhandled failure. Returning 500 for “missing email” hides the client fix and pollutes alerts. Use 400 with {"field":"email","error":"required"} so callers know what to change. See MDN 500.

How do I debug a 500 quickly?

Grab the response’s X-Request-Id (or timestamp) → search server logs for that ID → stack trace → line that threw. Add input validation before it so next time it’s 400, not 500. Return 500 {requestId} to clients, log the stack internally — don’t leak it per RFC 9110.

Is 403 always about permissions?

Not always — it can also be IP allowlist, rate limit (though 429 is correct for rate limit), or WAF block. Check body: {"error":"IP not allowed"} vs {"requires role: admin"} vs CORS headers. The fix depends on which — IP vs role vs origin.

How do I avoid leaking data in error responses?

Don’t return stack traces or SQL errors to clients — log them, return generic 500 {error: "Internal Server Error", requestId}. For 4xx, return field-level errors without exposing internals. This is both security and debuggability best practice per RFC 9110.

Last updated: September 3, 2026 • Author: Toolwasp Team • Sources verified Sep 3, 2026: MDN HTTP Status, MDN 400, MDN 401, MDN 403, MDN 500, RFC 9110, RFC 7231, Toolwasp JSON tools.