The HTTP Status Codes You're Probably Using Wrong (And How to Fix Them)
How many times have you inspected an API response in production, only to find: HTTP/1.1 200 OK { "code": 403, "status": "error", "message": "User not authorized" } Or an endpoint returning 400 Bad Request
How many times have you inspected an API response in production, only to find:
HTTP/1.1 200 OK
{
"code": 403,
"status": "error",
"message": "User not authorized"
}
Or an endpoint returning 400 Bad Request for everything from invalid JSON syntax to failed business logic rules?
HTTP status codes are the standardized language of the web. When used correctly, they make client-side error handling trivial, improve reverse proxy caching (Cloudflare, Nginx), and save engineers hours of debugging. When misused, they turn API integrations into a guessing game.
Let's dive into the most frequently confused status codes, their real RFC definitions, and how to apply them cleanly in your APIs.
1. 401 Unauthorized vs. 403 Forbidden
This is by far the most abused pair in modern API design — largely because HTTP's original naming in RFC 9110 is somewhat misleading.
+---------------------------------------------------------+
| Client sends a request |
+---------------------------------------------------------+
|
Does the server know who you are?
(Valid token / session / API key?)
/ \
NO YES
/ \
401 Unauthorized Is the user allowed
(Better name: Unauthenticated) to touch this resource?
/ \
NO YES
/ \
403 Forbidden 200/201/etc.
-
401 Unauthorized actually means Unauthenticated. The client has not provided valid credentials (missing
Authorizationheader, expired JWT, or invalid API key). Crucially, a401response typically includes aWWW-Authenticateheader telling the client how to authenticate. - 403 Forbidden means: “I know exactly who you are, but you don't have permission to do this.” Sending new authentication tokens will not change the outcome unless the user's role or permissions change.
💡 Rule of thumb: If logging in again might fix it, use
401. If logging in won't help because your account lacks admin privileges, use403.
2. 400 Bad Request vs. 422 Unprocessable Content
Before RFC 9110 (and RFC 4918 WebDAV), teams dumped every validation failure into 400. Modern REST design prefers separating syntax from semantics:
- 400 Bad Request: The server cannot parse the request at all. This means malformed JSON (missing closing bracket), invalid query string encoding, or invalid HTTP headers. The payload literally cannot be parsed by the deserializer.
- 422 Unprocessable Content (formerly Unprocessable Entity): The syntax is completely valid (clean JSON, well-formed fields), but the business validation failed.
// Example: POST /api/users
// Payload sent: {"email": "not-an-email", "age": -5}
// The JSON parsed successfully, but the fields are invalid.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"errors": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be greater than 0" }
]
}
Separating these two allows frontend clients to easily route 422 responses directly to UI form validation states, while treating 400 as an unexpected client-side bug.
3. The Redirect Quagmire: 301/302 vs. 307/308
Back in the early days of HTTP/1.0, browsers did something unintended: when they received a 301 Moved Permanently or 302 Found in response to a POST request, they would rewrite the follow-up request method to GET.
This behavior became de facto standard, breaking form submissions and API mutations. To resolve this without breaking legacy websites, the IETF introduced 307 and 308:
| Code | Name | Semantic | Preserves POST/PUT Method? |
|---|---|---|---|
| 301 | Moved Permanently | Resource permanently at new URL | ❌ Usually rewritten to GET
|
| 302 | Found | Temporary redirect | ❌ Usually rewritten to GET
|
| 307 | Temporary Redirect | Temporary redirect | ✅ Guaranteed to preserve method & body |
| 308 | Permanent Redirect | Permanent redirect | ✅ Guaranteed to preserve method & body |
💡 In modern APIs: If you enforce HTTPS redirects or route versioning (e.g.,
/api/v1/checkout→/api/v2/checkout), use 308 Permanent Redirect or 307 Temporary Redirect. Using301or302on a POST payload will result in the client sending an emptyGETto the new endpoint!
4. 502 Bad Gateway vs. 504 Gateway Timeout
When debugging backend infrastructure behind a reverse proxy (Nginx, Envoy, Cloudflare, AWS ALB), these two indicate completely different root causes:
- 502 Bad Gateway: The reverse proxy contacted the upstream service (e.g., your Node/Python/Go container), and the upstream sent an invalid or abruptly closed response (e.g., upstream process crashed with an OOM error, segfaulted, or returned corrupt HTTP).
-
504 Gateway Timeout: The reverse proxy contacted upstream, but the upstream took too long to respond (exceeded
proxy_read_timeoutor ALB timeout). Often caused by unindexed database locks, third-party API stalls, or CPU starvation.
5. When to Return 204 No Content
204 No Content is designed for actions that succeed but have nothing meaningful to return in the response body.
-
Good use cases:
-
DELETE /api/items/42: The item is gone; returning empty body with204is clean. -
PUT /api/users/me/preferences: Saved successfully, and the client already knows what state it sent.
-
-
Watch out for:
- A
204response must never include a message body (not even{}). - If your frontend relies on receiving the updated object timestamp or auto-generated fields (like
updated_at), return200 OKwith the entity instead.
- A
A Handy Reference for Everyday Debugging
With over 60 standardized status codes across RFC 9110 and related extensions (from 206 Partial Content for video streaming to 429 Too Many Requests with Retry-After), keeping track of exact caching semantics, headers, and troubleshooting steps is tough.
When designing APIs or debugging production logs, I frequently reference this HTTP Status Codes Directory. It has quick RFC references, cause-and-solution breakdowns, and raw response snippets for every status code without the clutter of reading through 200-page IETF specs.
Quick Summary Cheat-Sheet
1xx -> Hold on (Protocol upgrades, Expect: 100-continue)
2xx -> Here you go (200 OK, 201 Created, 204 No Content)
3xx -> Go over there (Use 307/308 for APIs to preserve POST/PUT)
4xx -> You (the client) messed up (401 unauthenticated, 403 forbidden, 422 validation)
5xx -> We (the server/infra) messed up (502 crashed upstream, 504 timed out upstream)
What's your API design pet peeve?
Do you enforce 204 on deletes, or prefer returning { "success": true }? How do you structure your error response schemas? Let's discuss in the comments below!
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.