L2 · Engineering literacy
L2Core5 min read

HTTP, REST, status codes, headers

Requests, responses, idempotency, caching headers, what a 409 actually means. Every API test, every network-tab debugging session and every mock you write sits on this. Shallow knowledge here shows up as flaky tests you cannot explain.

Almost every automated test in a modern suite eventually talks to an API over HTTP, whether that is the test driving the app directly or the app itself calling a backend under the hood. Testers who only know "GET fetches, POST creates" miss the details that actually cause production bugs: wrong idempotency assumptions, cache headers nobody checked, and status codes used inconsistently across a codebase. This guide covers what actually matters for testing, not the full HTTP spec.

The verbs and what they promise

REST's verbs are contracts, not just routing labels. GET must be safe, meaning it should never change server state, which is why a GET request that increments a view counter as a side effect is a design smell worth flagging in review. POST creates a new resource or triggers a non-idempotent action, and calling it twice can create two records.

PUT and DELETE are meant to be idempotent: calling them ten times should produce the same end state as calling them once. PATCH sits in between, applying a partial update, and its idempotency depends entirely on what the patch actually contains. A test suite that assumes idempotency without verifying it is testing an assumption, not the API.

Status codes worth actually distinguishing

Teams that lump every failure into a generic 400 or 500 make debugging harder for everyone downstream. The codes that carry the most testing value:

  • 401 vs 403: 401 means "we do not know who you are" (missing or invalid credentials); 403 means "we know who you are, and you cannot do this" (authorization, not authentication). Confusing them makes auth bugs harder to triage.
  • 404 vs 410: a 404 leaves open whether the resource ever existed; a 410 explicitly says it existed and is gone. Few APIs bother with the distinction, but it matters for cache invalidation logic.
  • 409: a conflict, usually from a concurrent update or a uniqueness constraint. A test suite that never triggers a 409 has never tested concurrent writes.
  • 429: rate limiting. A suite hitting a real API needs a documented backoff strategy, or it will trip this constantly during full regression runs.
  • 5xx: anything here is a server bug by definition. A test that expects a 500 as "expected behavior" for bad input is testing the wrong thing; that input should return a 4xx.

Headers that quietly control behavior

Content-Type and Accept decide what gets serialized and how, and a mismatch between them is a common source of confusing 415 errors. ETag and If-None-Match drive conditional requests: a client sends the ETag it already has, and the server returns 304 Not Modified if nothing changed, saving a full payload transfer.

Cache-Control headers determine whether a response can be cached by a browser, a CDN, or an intermediate proxy, and testing them requires more than checking the response body. A response that should never be cached (an account balance, a one-time token) but ships with a permissive Cache-Control: public, max-age=3600 header will serve stale, and sometimes wrong, data to a second user behind a shared proxy.

A worked example: the cache header nobody tested

Consider a retail team shipping a "check inventory" endpoint used on a product page. The endpoint's response carried a default Cache-Control: public, max-age=300 header inherited from the API gateway's default configuration, and nobody in the team had reviewed it since the endpoint's early prototype stage.

The test suite verified the JSON body's correctness on every run and never inspected response headers. For months this was invisible, because most traffic hit the origin directly. Then the company put a CDN in front of the API for a traffic spike, and the CDN faithfully cached the inventory response for five minutes per unique URL. Customers on a fast-selling item saw "in stock" long after it sold out, and support tickets spiked before anyone traced it to a header the test suite had never looked at.

The fix was two-fold: setting Cache-Control: no-store on any endpoint carrying live state, and adding a contract test that asserts cache headers on a fixed list of "must never cache" routes. That second part is what would have caught the original misconfiguration before it ever reached the CDN.

What this means for API test design

A test suite exercising REST endpoints should assert on status code, on response body shape (ideally via schema, covered in API testing and schema validation), and increasingly on headers that affect caching and content negotiation. Skipping headers because "the body was right" is how bugs like the inventory example ship unnoticed.

REST endpoints are also frequently guarded by tokens, and the auth: sessions, JWT, OAuth2, OIDC fundamentals apply directly here: a 401 versus 403 distinction only means something if your test suite understands which one it should expect for a given credential state.

FAQ

Questions people ask

Do I need to test every status code an API can return?

No, but you should test every status code your app's UI or downstream logic branches on. If the app treats 401 and 403 differently, your suite needs both cases covered.

Is GraphQL a replacement for testing REST fundamentals?

No, the underlying transport is still HTTP with its own considerations, and the same status-code and header discipline still applies once you add a query layer on top.

How do I test idempotency without a lot of manual setup?

Fire the same request twice in a loop as part of your existing API test and assert the resulting state (row count, side effect count) is unchanged between the first and second call.

Should test environments use the same cache headers as production?

Yes, ideally through the same gateway or proxy configuration, otherwise a caching bug will only surface after release, exactly like the inventory example above.