DEV Community

Miller Elias
Miller Elias

Posted on

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"
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
  • 401 Unauthorized actually means Unauthenticated. The client has not provided valid credentials (missing Authorization header, expired JWT, or invalid API key). Crucially, a 401 response typically includes a WWW-Authenticate header 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, use 403.

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" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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. Using 301 or 302 on a POST payload will result in the client sending an empty GET to 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_timeout or 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 with 204 is clean.
    • PUT /api/users/me/preferences: Saved successfully, and the client already knows what state it sent.
  • Watch out for:
    • A 204 response 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), return 200 OK with the entity instead.

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)
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)