Skip to content
Control Plane Labs

200 OK

Success response defined by RFC 9110 §15.3.1.

Written and maintained by Ben Ennis

Last reviewed July 27, 2026 · How we verify this

Common causes at a glance

  • The request succeeded and the server returned a representation
  • An API returning application-level errors inside a 200 envelope
  • A catch-all route or SPA fallback serving index.html for a URL that does not exist
  • A load balancer or error page short-circuiting the origin with a 200 body

Reproduce it

curl -i https://www.example.com/

What 200 OK means

The important thing about 200 is how little it promises. It confirms the server understood the request and produced a representation. What that representation means depends on the method: for GET it is the target resource, for POST the result of the action, for HEAD the headers a GET would have returned with no body.

That gap is where most 200-related bugs live. An API returning {"error": "not found"} with a 200 is well-formed HTTP and useless to every cache, retry policy, and dashboard in the path. Status codes are the only part of the response intermediaries reason about.

What the spec says

RFC 9110 §15.3.1 defines 200 and lists payload semantics per method. Two consequences are easy to miss. A 200 to GET or HEAD is heuristically cacheable unless the method definition or explicit cache directives say otherwise (RFC 9111 §4.2.2), which is why an accidental 200 on an error page can be served from a CDN for hours. And a 200 to a request that created something should have been 201 with a Location header (§10.2.2).

What actually causes it

Nobody debugs a legitimate 200. The interesting failure is a 200 that should have been something else. Single-page-app hosting is the biggest offender: a catch-all rewrite serving index.html for every unmatched path turns broken deep links into 200s, and search engines index thousands of empty pages as a result. GraphQL is second, since the spec puts errors in the response body, so a resolver blowing up still returns 200 and alerting sees a healthy error rate. Third are frameworks that wrap everything in {"success": false}. All three break retries, circuit breakers, and cache invalidation, because none of those look inside bodies.

How to debug it

When a 200 is suspicious, check the body length and Content-Type first. A soft 404 gives itself away as an HTML document on a path that should return JSON:

curl -s -o /dev/null -w '%{http_code} %{content_type} %{size_download}\n' https://api.example.com/v1/nope

For SPA hosting, compare a known-good path against a garbage path. If both return 200 with the same byte count, the rewrite rule is catching everything and you need a real 404 branch for API prefixes and asset extensions. For APIs, grep logs for 200 responses whose body contains an error key, then fix the handler rather than the monitoring. The status code lookup is a quick way to compare what an endpoint actually sends.

Why a 200 can mean the server ignored your Range header

A client asking for part of a resource sends Range: bytes=0-999 and expects 206 back. RFC 9110 §14.2 is explicit that a server MAY ignore the Range header field, and when it does, the correct response is a full 200 with the entire body, not an error. A download manager or video player that only checks for success rather than the status code will silently re-fetch the whole file instead of resuming, and the bug looks like a slow network rather than a missing feature.

This matters more than it seems because range support is opt-in per resource, not per server. A CDN in front of dynamically generated content commonly serves ranges for static assets and ignores them for anything passing through an application origin, so the same site returns 206 for a JavaScript bundle and 200 for a report export a client expected to resume. Check Accept-Ranges: bytes on the resource in question before assuming a client bug; its absence, or a 200 in response to a Range request, both mean the same thing: nobody is going to give you partial content here.

RFC 9110 §14.2 also requires a server to ignore a Range header on any method other than GET, and to ignore a range unit it does not recognise, both silently, both resulting in an ordinary 200. If you are debugging a resumable upload or download that keeps restarting from zero, confirm with curl -i -H ‘Range: bytes=0-99’ … that the response is actually a 206 with a Content-Range header before looking anywhere else.

# 206 with Content-Range means ranges are honoured; 200 means they were ignored.
curl -sD - -o /dev/null -H 'Range: bytes=0-99' https://www.example.com/report.csv \
  | grep -iE '^(HTTP/|content-range|accept-ranges)'

Working examples

curl

# The shape of a 200 matters more than the code. Print the useful parts.
curl -s -o /dev/null \
  -w 'status=%{http_code} type=%{content_type} bytes=%{size_download} time=%{time_total}\n' \
  https://www.example.com/

# Headers only, without downloading the body:
curl -sI https://www.example.com/

Python (requests)

import requests

resp = requests.get("https://api.example.com/v1/widgets/123", timeout=10)

# 200 alone is not success for an API that reports errors in the body.
resp.raise_for_status()  # only catches 4xx/5xx
body = resp.json()
if isinstance(body, dict) and body.get("error"):
    raise RuntimeError(f"soft failure behind 200: {body['error']}")

print(resp.status_code, resp.headers.get("Content-Type"), len(resp.content))

Node (fetch)

// Detect soft 404s: a 200 that hands back HTML where JSON was expected.
const res = await fetch("https://api.example.com/v1/widgets/123");
const type = res.headers.get("content-type") ?? "";

console.log(res.status, type, res.headers.get("content-length"));

if (res.ok && !type.includes("application/json")) {
  console.warn("200 with unexpected content type - likely an SPA fallback");
}
console.log(await res.text());

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 2xx codes