Skip to content

HTTP status codes and methods

HTTP (Hypertext Transfer Protocol) response status codes

Section titled “HTTP (Hypertext Transfer Protocol) response status codes”

Status codes tell the client what happened and drive retries, caching, and UI (User Interface) behavior. For machine clients (mobile apps, microservices), prefer stable semantics over clever overloads: if two outcomes need different handling, use different codes (or distinct problem types in the body).

Abbreviations: HTTP, JSON (JavaScript Object Notation), REST (Representational State Transfer), API (Application Programming Interface), RFC (Request for Comments), CORS (Cross-Origin Resource Sharing), ETag (entity tag), RBAC (Role-Based Access Control), UI, SPA (Single-Page Application), TLS (Transport Layer Security), OAuth (Open Authorization), CDN (Content Delivery Network), S3 (Amazon Simple Storage Service), SEO (Search Engine Optimization).

The first digit is the class (RFC 9110):

ClassRangeMeaning (for APIs)
Informational1xxProtocol housekeeping; rare in typical JSON REST over HTTP/1.1 or HTTP/2.
Success2xxRequest understood and accepted; response body semantics depend on the code.
Redirection3xxResource lives elsewhere or not modified; clients must follow rules for method/body.
Client error4xxFix the request, auth, or permissions; usually do not retry the same payload blindly.
Server error5xxOrigin failed; safe retries with backoff may be appropriate for idempotent reads.

plantuml


In practice: Most JSON APIs never return these to application code; the stack handles them.

CodeNameWhen it appears
100ContinueClient sent Expect: 100-continue; server allows body upload.
101Switching ProtocolsWebSocket upgrade path (not a “JSON 200” style response).

Real-world: Large file uploads to S3-compatible APIs using 100-continue; WebSocket handshakes returning 101.


CodeMeaningUse for
200OKDefault success: GET with body, PUT/PATCH that returns updated resource, POST when returning the created/processed entity without needing 201 semantics.
201CreatedPOST created a new resource; include Location when the new URL is stable and meaningful.
202AcceptedWork queued (async job, webhook fan-out); body often has job id + poll URL.
204No ContentSuccess with no response body: DELETE, or PUT/PATCH where the client already has the final state.
206Partial ContentRange requests for large downloads (video, firmware); include Content-Range.

Avoid

  • 201 without a clear new resource (confuses caches and clients that branch on “created”).
  • 204 when the client needs the server’s final representation (use 200 + body instead).

Real-world examples

  • 200: GET /users/42 returns profile JSON.
  • 201: POST /orders returns order JSON and Location: /orders/991.
  • 202: POST /exports starts CSV generation; client polls GET /exports/jobs/7 until 200 with a download URL.
  • 204: DELETE /sessions/current — session gone; nothing to return.

plantuml


Redirects matter for browsers and HATEOAS; service-to-service clients often disable auto-follow or require explicit configuration.

CodeTypical useClient note
301Permanent move (URL changed forever).Historically GET-oriented; changing method caused pain—prefer 308 for “same method forever” when you control clients.
302Temporary redirect (found elsewhere).Some stacks historically turned POST into GET on follow—do not rely on that for APIs; prefer 303 or 307.
303After POST, see other resource (GET the Location).Common in HTML forms; useful for PRG pattern (Post/Redirect/Get).
304Not ModifiedConditional GET (If-None-Match / If-Modified-Since); empty body; client keeps cached representation.
307Temporary redirect; preserve method.Safer default than 302 for APIs when you must redirect POST.
308Permanent redirect; preserve method.SEO + API versioning moves (/v1/.../v2/...) with same verb semantics.

Real-world examples

  • 304: Mobile app GET /config with ETag (entity tag)—server returns no body when unchanged, saving bandwidth.
  • 308: API gateway permanently moves /legacy/foo to /v2/foo for all methods.

The client (or caller) should change something: credentials, URL, body, or timing.

CodeMeaningUse for
400Bad RequestMalformed JSON, wrong types, missing required field before domain rules (generic “cannot parse / validate shape”).
401UnauthorizedNot authenticated (missing/invalid token).
403ForbiddenAuthenticated but not allowed (RBAC, tenant isolation).
404Not FoundNo resource at that identifier or you intentionally hide existence (404 vs 403 is a product/security choice).
405Method Not AllowedGET on a URL that only allows POST; send Allow header.
409ConflictVersion conflict, duplicate unique key, illegal state transition (e.g. cancel shipped order).
412Precondition FailedIf-Match ETag / version precondition failed.
413Payload Too LargeBody over limit; client must chunk or use upload URL.
415Unsupported Media TypeWrong Content-Type (e.g. XML to a JSON-only endpoint).
422Unprocessable Content (RFC 9110)Shape is valid JSON but business validation failed (date in past, unsupported country).
428Precondition RequiredServer requires conditional headers (optimistic locking policy).
429Too Many RequestsRate limit; include Retry-After when possible.
451Unavailable For Legal ReasonsGeo or compliance block.

401 vs 403 (mnemonic)

  • 401 — “Who are you?” (authenticate)
  • 403 — “I know who you are; you still cannot do this.” (authorize)

404 vs 403 (privacy)

  • Public APIs sometimes return 404 for private resources so attackers cannot probe “exists but forbidden.”
  • Admin consoles often return 403 for clearer operator UX.

Real-world examples

  • 409: Two tabs checkout the last inventory unit; second POST /checkout gets 409 with INSUFFICIENT_STOCK.
  • 422: POST /bookings with valid JSON but endDate before startDate.
  • 429: API gateway throttles a misconfigured client; exponential backoff respects Retry-After.

plantuml


Indicates the server failed after accepting a valid request. Clients may retry idempotent reads; for POST, retries need idempotency keys or deduplication to avoid duplicates.

CodeMeaningUse for
500Internal Server ErrorUnexpected bug, uncaught exception—log correlation id; avoid using as generic “any error.”
501Not ImplementedFeature not built (prefer 404 or 405 if the route should not exist).
502Bad GatewayGateway/proxy got invalid response from upstream.
503Service UnavailableOverload, maintenance; use Retry-After when known.
504Gateway TimeoutUpstream too slow; client may retry with backoff.
507Insufficient StorageRare in HTTP APIs; more common in WebDAV-style systems.

Real-world examples

  • 502: Load balancer cannot reach app pods during a rollout.
  • 503: Database failover in progress; maintenance window.
  • 504: GET through API gateway waits for a microservice that hangs.

For 4xx/5xx, return a consistent JSON shape (or RFC 7807 Problem Details):

  • type — URI identifying the problem category (stable for clients).
  • title — short human summary.
  • status — repeat the HTTP code.
  • detail — specific explanation (avoid leaking secrets).
  • instance — optional correlation id or request id.

This complements the status code: the code drives transport behavior; the body drives product behavior.


Methods describe intent. Safe methods should not change server state; idempotent methods should leave the server in the same state if repeated.

MethodTypical intentSafeIdempotent
GETRead resource(s)YesYes
HEADSame as GET without bodyYesYes
POSTCreate resource, or trigger action / RPCNoNo (unless designed with idempotency keys)
PUTReplace entire resource at URLNoYes
PATCHPartial updateNoNot guaranteed (depends on patch semantics)
DELETERemove resourceNoYes
OPTIONSCORS (Cross-Origin Resource Sharing) preflight or discoveryYesYes

Nuances (API design)

  • POST is not only “insert row”: it is also used for search (POST /search with complex body), actions (POST /orders/9/cancel), and OAuth token endpoints—document whether each POST is safe to retry.
  • PUT vs POST for create: POST to collection (/items) with server-assigned id → 201; PUT to known URL (/items/client-uuid) can be idempotent create-or-replace.
  • PATCH: Prefer JSON Merge Patch (RFC 7396) or JSON Patch (RFC 6902) and document conflict behavior (409 / 412).

plantuml


A preflight is an extra OPTIONS request the browser sends before the “real” request, when that real request is cross-origin and not “simple”. The browser waits for a successful CORS response on preflight; only then does it send GET / POST / PUT / … with your JSON body and Authorization header.

This is not your REST error model: your API may return 200 JSON from OPTIONS, but browsers primarily care about Access-Control-* response headers, not the body. If preflight fails, your JavaScript never sees the actual 401/422 from the API—the request is blocked at the network layer and DevTools shows a CORS error.

In browser / Fetch terminology, yes: the word preflight (Fetch spec) names the CORS-specific OPTIONS round-trip the user agent performs before certain cross-origin requests. When developers say “the preflight,” they mean that mechanism—not every HTTP OPTIONS on the internet.

Nuances

QuestionAnswer
Is every OPTIONS request a CORS preflight?No. OPTIONS is a normal method: tools (curl), proxies, or WebDAV-style stacks may call it for discovery (Allow: …) or other behavior without Origin / Access-Control-Request-*.
Does every cross-origin browser request use a preflight?No. Simple requests (safelisted method + headers + Content-Type) skip preflight; the browser still checks Access-Control-Allow-Origin on the actual response.
Is preflight only a browser thing?Yes. Non-browser clients do not perform CORS preflight.

So: preflight ⊂ OPTIONS, and preflight = CORS preflight in web API docs. CORS itself is broader (includes responses to simple GET without a prior OPTIONS).

Browser-enforced vs “application” (server) role

Section titled “Browser-enforced vs “application” (server) role”

Preflight and CORS are browser-enforced for code running in a normal web context (pages using fetch, XMLHttpRequest, some Web APIs). The browser decides whether to send the preflight, whether to send the real request after OPTIONS, and whether your page’s JavaScript is allowed to read the cross-origin response body and headers. That enforcement lives in the user agent, not in your Node/Java/Go process as a mandatory gate.

Your API / gateway “application” does not enforce CORS on the internet the way a firewall enforces rules. It opts in by returning Access-Control-* headers so cooperating browsers permit front-end access. Any client that is not a browser (curl, Postman, mobile native apps, server-to-server calls, malware) can call your API without sending Origin or honoring preflight; those requests are unaffected by CORS. Real API security still comes from authentication, authorization, TLS, rate limits, and input validation—not from CORS alone.

Summary

LayerRole
BrowserEnforces preflight + same-origin policy for web page scripts; blocks JS from reading disallowed cross-origin responses.
ServerPublishes policy via Access-Control-* so allowed origins can use the API from a browser. Does not stop arbitrary non-browser clients.

Browsers apply CORS when the request’s origin (scheme + host + port) differs from the resource’s origin—for example:

  • Page: https://app.example.com
  • API: https://api.example.comcross-origin (different host)
  • Page: https://app.example.com:3000 vs API https://app.example.com:443cross-origin if ports differ

Same host and scheme and port → same-origin; typical navigation and fetch to your own origin do not need CORS preflight for normal cases.

If the request is cross-origin and the browser classifies it as simple, it is sent immediately—no OPTIONS first.

Roughly, a simple request is:

ConstraintAllowed (typical)
MethodGET, HEAD, or POST only
HeadersOnly CORS-safelisted headers (e.g. Accept, Accept-Language, Content-Language, and Content-Type with values below)
Content-Type (for POST)application/x-www-form-urlencoded, multipart/form-data, or text/plain only

Examples that stay simple

  • GET https://api.example.com/public/status from https://app.example.com with no extra headers.
  • POST form application/x-www-form-urlencoded with no custom headers.

Examples that trigger preflight (non-simple)

  • Authorization (Bearer token), X-Request-Id, X-CSRF-Token, or any custom header not in the safelist.
  • Content-Type: application/json on POST/PATCH (JSON APIs almost always preflight).
  • Methods PUT, PATCH, DELETE, etc.

So: most SPAs calling a JSON API with a Bearer token preflight on almost every call.

The browser issues:

OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

Meaning: “If I later send POST from origin https://app.example.com with these headers, is that allowed?”

The server (or API gateway) should respond with 204 or 200 and no required body, but must include CORS headers, for example:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Access-Control-Max-Age: 86400
  • Access-Control-Allow-Origin: which page origins may read the response. Cannot be * when the browser sends credentials (cookies / client certs); must echo a specific origin.
  • Access-Control-Allow-Methods: methods allowed for the actual request.
  • Access-Control-Allow-Headers: which request headers the real call may include (must cover everything listed in Access-Control-Request-Headers).
  • Access-Control-Max-Age: how long (seconds) the browser may cache this preflight result so it does not repeat OPTIONS on every click (see below).

If Allow-Origin is missing, wrong, or Allow-Headers omits authorization, the preflight fails and the real request is never sent.

The browser then sends the actual request (POST, body, Authorization, etc.). That response also needs Access-Control-Allow-Origin (and, if credentials are used, Access-Control-Allow-Credentials: true) or the browser will hide the response body from JavaScript even though the server “succeeded.”

plantuml

Browsers cache a successful preflight outcome keyed roughly by URL + origin + method + requested headers. While the cache entry is valid (up to Max-Age, capped per browser), the browser may skip the OPTIONS round-trip and send POST/PUT directly.

  • Higher Max-Age (e.g. 86400): fewer OPTIONS calls, faster repeat visits.
  • Zero or omitted: preflight may repeat often (still subject to browser defaults).

Changing CORS policy on the server does not invalidate clients instantly; operators may need to wait out Max-Age or test in a fresh profile during CORS migrations.

How this differs from “normal” OPTIONS

Section titled “How this differs from “normal” OPTIONS”

Many APIs implement OPTIONS for discovery (e.g. listing methods on a resource). CORS preflight is a browser-enforced contract: the request comes from the browser with Origin and Access-Control-Request-*; the response must carry Access-Control-Allow-*. You can satisfy both by returning the same Allow header and CORS headers on OPTIONS.

An OPTIONS request is ordinary HTTP: it hits your TLS terminator, then often a CDN, reverse proxy, API gateway, and finally your application—unless an earlier layer answers it and stops the chain.

Typical pattern (preflight)

  1. A layer matches OPTIONS (globally, per path prefix, or per route).
  2. It returns 204 No Content or 200 OK with an empty or ignored body.
  3. It adds Access-Control-* headers from static config or a small allowlist function (allowed origins, methods, headers, optional Max-Age).
  4. The request never reaches heavy handlers (no DB query for the business operation). That keeps preflight cheap and fast.

Where it is implemented in real systems

LayerWhat it does
CDN / edge (Cloudflare, Fastly, CloudFront)Transform rules or workers respond to OPTIONS at the edge so origin never sees them.
Reverse proxy (nginx, HAProxy, Envoy)if ($request_method = OPTIONS) { ... return 204; } plus add_header Access-Control-*.
API gateway (Kong, AWS API Gateway, Apigee)Built-in CORS plugin adds headers on OPTIONS (and sometimes on real methods).
Application framework (Express cors, Spring CorsFilter, ASP.NET CORS, FastAPI CORSMiddleware, etc.)Middleware runs before your controller; for allowed origins it short-circuits with 204 + headers.

plantuml

Discovery OPTIONS (optional): the same verb can return Allow: GET, POST, HEAD (and a body listing capabilities). CORS preflight does not require Allow, but adding it is fine. The browser still requires Access-Control-Allow-* for cross-origin use.

Common mistakes

  • No route for OPTIONS → proxy returns 404/405 without CORS headers → preflight fails.
  • Requiring Authorization on OPTIONS → browser often does not send your Bearer token on the preflight; the request fails before the real POST. CORS middleware should allow unauthenticated OPTIONS while still enforcing auth on POST/GET.
  • Only adding CORS on OPTIONS but forgetting Access-Control-Allow-Origin on GET/POST responses → preflight passes but the real response is still blocked from JS.
  1. Gateway or app responds to OPTIONS on API routes (or delegates to a CDN/gateway that does).
  2. Access-Control-Allow-Headers includes every header your SPA sends (Authorization, Content-Type, tracing headers).
  3. Allow-Methods includes OPTIONS plus real verbs (PATCH, DELETE, …).
  4. Allow-Origin matches your front-end origins in non-prod and prod (avoid reflecting arbitrary Origin unless you understand the security model).
  5. Credentials: if cookies cross subdomains, you need Allow-Credentials: true and a specific Allow-Origin (not *).

Cross-Origin Resource Sharing lets https://app.example.com call https://api.example.com. Misconfigured CORS surfaces as browser failures (no response to JS), not always as 4xx JSON. Configure allowed origins, methods, and headers explicitly; avoid * with credentials. See Preflight requests for how the browser validates cross-origin calls before your POST runs.

Inbound HTTP callbacks (Stripe, GitHub): your server returns 2xx quickly and processes async; use 429/Retry-After if overloaded so senders backoff. Verify signatures (HMAC) before trusting body.

Upgrade from HTTP (101); after upgrade, traffic is frames, not classic request/response status codes per message—different observability and error model.

Signaling may use HTTP/JSON, but media is peer-to-peer; status codes apply to the signaling HTTP API, not to each media packet.

CORS preflight is one of several “extra steps before the main payload” patterns in HTTP stacks. They differ in who runs them (browser vs any client) and what is being negotiated.

PatternWho drives itWhat happens (short)
DNS lookupResolver (OS / browser / DoH)Hostname → IP (cached TTL); must succeed before TCP.
TCP handshakeClient OS + serverSYN / SYN-ACK / ACK; reliable byte stream before TLS or HTTP.
TLS handshakeClient + serverCertificates, key agreement, ALPN (h2 vs http/1.1) before encrypted HTTP.
HTTP/2 / HTTP/3 setupStackHTTP/2: SETTINGS frames; HTTP/3: QUIC + TLS 1.3 before request streams.
Expect: 100-continueClient (often SDKs)Client may wait for 100 Continue before uploading a large body (e.g. object storage); extra application-level round-trip.
WebSocket upgradePage + browserHTTP request with Upgrade: websocket101 Switching Protocols, then frames (not REST JSON per hop).
401 + WWW-AuthenticateServer challenge; client retriesClient may send another request with Authorization (Basic/Digest); shape is challenge → retry, not CORS.
Service Worker fetchBrowserSW can short-circuit the network, synthesize a response, or modify the outgoing request.
HSTSBrowser policyInternal upgrade to HTTPS (or hard fail) before the visible request URL is hit—another browser-only gate.

Only CORS preflight uses OPTIONS with Origin / Access-Control-Request-* to decide whether cross-origin JavaScript may see the response.


End-to-end workflow (browser → origin, composite)

Section titled “End-to-end workflow (browser → origin, composite)”

The diagram below is intentionally complete: a real call only follows one path through it. For example, a simple cross-origin GET skips preflight but still needs Access-Control-Allow-Origin on the response; a browser cache hit can skip DNS/TLS entirely; curl skips CORS and Service Workers.

plantuml

  • Early stop in the diagram means the flow ended there for that request: Service Worker satisfied the fetch from cache, HTTP cache served without network, or CORS blocked (preflight failure or response not exposed to JavaScript).

  • DNS, TCP, TLS apply to any HTTP user agent, not only browsers.

  • CORS, Service Workers, HSTS are browser-side gates for normal web pages.

  • CDN is not its own swimlane; each hop uses the same HTTP semantics between browser and origin.

  • Preflight is only the CORS block (4); it does not replace TLS (3) or DNS (3).

  • Status codes (7) describe the main response after redirects settle; 401 on the main call is visible to JS only if CORS (8) also allows exposing the response.

  • Caching appears twice: browser HTTP cache (2) before the network, and cache headers on the response (8); CDNs behave similarly but sit between browser and origin (not drawn as a separate swimlane here—same HTTP semantics at each hop).


I want to…Prefer
Return a list or entity200 + body
Create with server id201 + Location + body
Queue async work202 + job metadata
Save bandwidth on unchanged GET304 + validators
Tell client to log in401
Tell client “not allowed”403 (or 404 if hiding existence)
Validation failed (business rules)422 (or 400 with strict generic policy)
Version / state conflict409 or 412
Rate limit429 + Retry-After
Bug / upstream outage5xx + correlation id in body