HTTP Status Codes
HTTP STATUS CODES ARE THE RESPONSE CONTRACT BETWEEN SERVER AND CLIENT — CORRECT CODE SELECTION ENABLES ERROR HANDLING, RETRY LOGIC, AND MONITORING WITHOUT PARSING RESPONSE BODIES. MISUSING STATUS CODES FORCES CLIENTS TO TREAT 200 OK AS AN AMBIGUOUS SIGNAL THAT MUST BE INSPECTED FOR HIDDEN FAILURES.
When to Use
- Designing the response contract for a new API endpoint
- Reviewing a PR that returns
200 OKfor validation errors or internal failures - Choosing between
404 Not Foundand403 Forbiddenwhen a resource exists but access is denied - Deciding whether to return
200or204after a successful mutation - Explaining why
422 Unprocessable Entityis more appropriate than400 Bad Requestfor semantic validation failures - Selecting the correct code for rate limiting, service unavailability, and conflict scenarios
- Building error monitoring dashboards that distinguish client errors (4xx) from server errors (5xx)
- Implementing retry logic that behaves correctly for 429, 503, and 500 responses
Instructions
Key Concepts
-
1xx Informational — Provisional responses sent before the final response. Rarely used in REST APIs.
100 Continueis sent by servers to indicate that the initial part of a request was received and the client should proceed.101 Switching Protocolsis used for WebSocket upgrades. -
2xx Success — The request was received, understood, and accepted. The three most important 2xx codes:
200 OK— General success with a response body. Use for GET, PATCH, and PUT when returning the updated resource.201 Created— A new resource was created. Must include aLocationheader with the URL of the new resource. Use for POST and PUT-to-create.204 No Content— Success with no response body. Use for DELETE and PUT/PATCH when returning the resource is not needed.
-
3xx Redirection — Further action is needed to complete the request.
301 Moved Permanentlyredirects clients and updates bookmarks.302 Found(temporary redirect) does not update bookmarks.304 Not Modifiedis the conditional GET response — seeapi-conditional-requests. APIs should avoid redirects in normal operation flows; they complicate client retry logic. -
4xx Client Error — The request contained an error the client must fix before retrying. These are non-retryable without change. Key codes:
400 Bad Request— Malformed syntax, invalid parameters, missing required fields.401 Unauthorized— No valid authentication credentials provided. The client should re-authenticate.403 Forbidden— Authentication succeeded but the caller lacks permission. Do not leak resource existence.404 Not Found— Resource does not exist at this URL, or the server is hiding its existence (use403if you want to reveal it exists).409 Conflict— Request conflicts with current resource state (e.g., duplicate creation, stale optimistic lock).422 Unprocessable Entity— Request is syntactically valid but semantically invalid (e.g., end date before start date).429 Too Many Requests— Rate limit exceeded. Must includeRetry-Afterheader.
-
5xx Server Error — The server failed to fulfill a valid request. These are potentially retryable. Key codes:
500 Internal Server Error— Unhandled exception or unexpected server failure. Do not expose stack traces.502 Bad Gateway— An upstream service returned an invalid response.503 Service Unavailable— The server is temporarily unable to handle requests. Should includeRetry-After.504 Gateway Timeout— An upstream service did not respond in time.
Worked Example
A GitHub REST API interaction demonstrating status code precision across a repository lifecycle:
Create a repository (POST → 201 Created):
POST /user/repos
Authorization: Bearer ghp_...
Content-Type: application/json
{ "name": "my-project", "private": true }
HTTP/1.1 201 Created
Location: https://api.github.com/repos/alice/my-project
Content-Type: application/json
{ "id": 123456, "name": "my-project", "full_name": "alice/my-project", ... }
Create duplicate repository (422 Unprocessable Entity — GitHub's choice):
POST /user/repos
Content-Type: application/json
{ "name": "my-project", "private": true }
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "Repository creation failed.",
"errors": [{ "resource": "Repository", "code": "custom", "field": "name",
"message": "name already exists on this account" }]
}
Note: GitHub returns 422 rather than 409 for duplicate names — a documented choice that treats name uniqueness as a semantic constraint rather than a state conflict.
Fetch without credentials (401 Unauthorized):
GET /repos/alice/my-project
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="GitHub"
Content-Type: application/json
{ "message": "Requires authentication" }
Fetch a private repo with wrong credentials (403 Forbidden):
GET /repos/alice/private-project
Authorization: Bearer ghp_wrong_token
HTTP/1.1 404 Not Found
GitHub returns 404 (not 403) to avoid leaking that the private repository exists — a security pattern called "security through obscurity on existence."
Rate limit exceeded (429 Too Many Requests):
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714780800
Content-Type: application/json
{ "message": "API rate limit exceeded for ghp_..." }
Anti-Patterns
-
Returning 200 OK for errors.
{ "success": false, "error": "User not found" }in a200 OKbody breaks monitoring, alerting, and client error handling. 5xx/4xx rates in logs become meaningless. Clients must parse every body to detect failure. Fix: use the appropriate 4xx or 5xx code with an error body conforming to RFC 9457 (Problem Details) orapi-problem-details-rfc. -
Using 404 when 403 is correct. If a resource exists and the caller lacks permission, returning
404(to hide existence) is a security decision that should be explicit and documented — not a default. It prevents clients from distinguishing "wrong URL" from "wrong permissions." Use403when the existence of the resource is not sensitive. Use404only when existence itself must be concealed. -
Using 400 for semantic validation errors.
400 Bad Requestsignals a malformed request (unparseable JSON, missing Content-Type, invalid URL parameter). Semantic failures — start date after end date, referenced resource does not exist, business rule violation — belong in422 Unprocessable Entity. This distinction helps clients route errors to the right handler: syntax errors (fix the request format) vs. semantic errors (fix the payload values). -
Returning 500 for client-caused failures. An API that throws a 500 when the request body contains unexpected values is a server bug, but returning 500 to the client incorrectly signals that the server is at fault. Validate inputs early, return 400/422 for client errors, and reserve 5xx for genuine server-side failures.
Details
The 401 vs 403 Distinction
This distinction is frequently confused:
401 Unauthorizedmeans the request lacks valid authentication. TheWWW-Authenticateheader tells the client how to authenticate. The fix: provide credentials.403 Forbiddenmeans authentication succeeded but authorization failed. The client is identified but not permitted. The fix: acquire the required permission or role.
The naming is historical — "Unauthorized" was named before authentication and authorization were cleanly separated in practice.
409 vs 422
409 Conflict— The request is valid but conflicts with the current state of the target resource. Use for optimistic concurrency failures (stale ETag), duplicate unique-key violations where idempotency is expected, or state machine violations (e.g., closing an already-closed order).422 Unprocessable Entity— The request is syntactically and structurally valid but fails semantic validation. Use for business rule violations, cross-field validation failures, and references to non-existent related resources.
Real-World Case Study: Stripe Error Taxonomy
Stripe's API maps all errors to HTTP status codes with machine-readable error codes in the body:
400for request parameter errors401for invalid API keys402for payment failures (a creative use of the rarely-used "Payment Required" code)403for permission errors404for non-existent resources409for idempotency key reuse with different parameters429for rate limits500/502/503for Stripe infrastructure failures
This precise taxonomy allows Stripe SDK clients to switch on error.type for business logic while using the HTTP status code for transport-level decisions (retry vs. no-retry). APIs that follow this pattern report 30-40% fewer support tickets related to error handling ambiguity.
Source
- MDN — HTTP Response Status Codes
- RFC 9110 — HTTP Semantics, Section 15
- RFC 9457 — Problem Details for HTTP APIs
- RFC 6585 — Additional HTTP Status Codes (429)
Process
- Identify the outcome category: success (2xx), client error (4xx), or server error (5xx).
- For 2xx: choose
201 Created+Locationfor resource creation,204 No Contentfor mutations with no return body,200 OKotherwise. - For 4xx: distinguish authentication failure (401), authorization failure (403), not found (404), state conflict (409), semantic validation failure (422), and rate limiting (429). For 429 and 503, always include
Retry-After. - For 5xx: return
500for unhandled server failures,503for intentional degraded-mode responses. Never expose stack traces or internal paths in 5xx bodies. - Run
harness validateto confirm skill files are well-formed.
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-http-methods, api-error-contracts, api-problem-details-rfc, api-rest-maturity-model
Success Criteria
- No endpoint returns
200 OKfor error conditions — all errors use appropriate 4xx or 5xx codes. - POST creation endpoints return
201 Createdwith aLocationheader. 401and403are used for authentication vs. authorization failure respectively, with documented rationale for any security-through-obscurity404substitutions.429responses include aRetry-Afterheader;503responses includeRetry-Afterwhen a recovery time is known.400is reserved for malformed requests; semantic validation failures use422 Unprocessable Entity.