The Anatomy of an HTTP Request: Headers, Methods, and Status Codes

Most developers interact with HTTP dozens of times a day without ever pausing to think about what actually travels across the wire. You click "Send" in Postman, a green 200 OK appears, and you move on. But the moment something breaks — a JWT gets rejected, a CORS preflight fails, a cache serves stale data at the worst possible time — you suddenly need to understand every byte of what left your machine and what came back. This guide is for that moment.

We're going to dissect a complete HTTP request and its response, field by field. Not a glossary, not a reference card — a real forensic walkthrough of what each piece controls and why getting it wrong costs you hours.

The Request Line: The First Three Words Matter Most

Every HTTP request opens with a single line that looks deceptively simple:

POST /api/v2/tokens HTTP/1.1

Three tokens. Method, path, protocol version. Each one carries real weight.

The method is not just semantic decoration. GET is supposed to be idempotent and safe — no side effects, cacheable by default. POST creates resources and is neither safe nor idempotent. PUT replaces an entire resource; PATCH modifies it partially. DELETE removes it. OPTIONS is what your browser sends before the actual request in a CORS preflight — a handshake to ask "are you willing to accept what I'm about to send?"

Where API testers go wrong: treating POST and PUT as interchangeable. A well-designed REST API treats them differently at the router level. If your update endpoint only accepts PUT and you send PATCH, you'll get a 405 Method Not Allowed — and that's the server being helpful. Some servers silently ignore the wrong method and return garbage. Know which verb your endpoint expects.

The protocol version matters more than most testers realize. HTTP/1.1 keeps the connection alive by default (persistent connections). HTTP/2 multiplexes multiple requests over a single TCP connection — which means header compression, parallel streams, and very different debugging behavior when things go sideways. If you're testing an API that behaves intermittently, check whether your client and server have negotiated different versions than you expect. curl --http2 -v makes this visible.

Request Headers: The Control Plane of HTTP

Headers are where the real configuration lives. After the request line, you have an unbounded set of key-value pairs that shape how the server interprets everything that follows.

Host

Host: api.example.com

Mandatory in HTTP/1.1. This is how virtual hosting works — a single IP can serve dozens of domains, and Host tells the web server which one you want. Get it wrong and you'll hit the default vhost, which could be anything. Proxy-heavy architectures sometimes rewrite this header, which causes authentication failures when the backend validates it against a stored value.

Authorization

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

For JWT-based APIs, this single header is the entire access control story. That long string is three Base64-encoded JSON objects joined by dots: header, payload, signature. The header names the algorithm (alg) and token type. The payload carries claims — sub (subject/user ID), iat (issued at), exp (expiration), aud (intended audience), and whatever custom claims your system adds.

A common testing trap: the token looks valid, but the server returns 401 Unauthorized. Decode the payload at jwt.io and check exp. Unix timestamp — if it's in the past, the token is expired regardless of how recently you generated it. Also check aud: if your token says "aud": "mobile-app" and you're hitting the web API endpoint that validates for "aud": "web-app", it will be rejected. This is intentional audience separation; it's also a frequent source of confusion when tokens are shared across environments.

Content-Type

Content-Type: application/json; charset=utf-8

Tells the server what format the request body is in. If you're sending JSON but this header says text/plain, many frameworks won't parse the body at all — your carefully constructed payload lands as raw bytes and your endpoint sees an empty object. Send multipart/form-data without the boundary parameter and file uploads silently fail. The charset matters too: UTF-8 is the safe default, but some legacy APIs choke on it and expect ISO-8859-1.

Accept

Accept: application/json, text/html;q=0.9, */*;q=0.8

Content negotiation — you're telling the server what response formats you can handle, in preference order. The q values are quality weights between 0 and 1. Most API clients just send Accept: application/json and be done with it, but knowing this exists explains why hitting the same endpoint in a browser sometimes returns HTML while your script gets JSON.

Cache-Control and ETag

Cache-Control: no-cache
If-None-Match: "33a64df551425fcc55e"

These two headers are how conditional requests work. Send If-None-Match with an ETag value from a previous response, and the server will return 304 Not Modified (with an empty body) if nothing has changed. This saves bandwidth and is exactly what browsers do for static assets — but it trips up API testers who don't expect a body-less response. The fix is simple: cache busting with Cache-Control: no-cache forces a fresh response every time, at the cost of not benefiting from server-side caching at all.

The Request Body: What You're Actually Sending

GET and HEAD requests have no body (by spec, even if some implementations accept it). POST, PUT, and PATCH carry their data here. The body's format must match the Content-Type header, and its length should match Content-Length if present — a mismatch causes the server to either reject the request or read the wrong number of bytes and produce baffling parse errors.

For API testing, watch for encoding pitfalls: special characters in JSON string values need escaping, dates should be ISO 8601 strings unless the API explicitly documents otherwise, and null versus absent field semantics differ between APIs. A field set to null and a field omitted entirely can mean very different things in PATCH operations — one explicitly clears a value, the other leaves it untouched.

The Response: What the Server Says Back

The Status Line

HTTP/1.1 200 OK

Status codes are grouped into five families. The groupings matter more than memorizing every code:

  • 1xx (Informational): Rare in practice. 100 Continue is the server telling your client to go ahead and send the body after an Expect: 100-continue header. 101 Switching Protocols is how WebSocket upgrades work.
  • 2xx (Success): 200 is the workhorse. 201 Created means a resource was created and you should get a Location header pointing to it. 204 No Content is success with no body — common for DELETE. 206 Partial Content is what video streaming uses for range requests.
  • 3xx (Redirection): 301 and 308 are permanent; 302 and 307 are temporary. The difference between 307 and 302: 307 guarantees the redirect uses the same HTTP method, while 302 in practice causes most clients to switch to GET. This matters enormously when you're POST-ing to a URL that redirects.
  • 4xx (Client Error): The problem is on your side. 400 Bad Request is vague — check the response body for details. 401 Unauthorized means your credentials are missing or invalid. 403 Forbidden means your credentials are fine but you lack permission — different problem. 404 resource not found. 422 Unprocessable Entity is what good APIs return when the JSON parses fine but the values fail validation. 429 Too Many Requests means rate limiting — check the Retry-After header.
  • 5xx (Server Error): The problem is on their side. 500 is a generic crash. 502 Bad Gateway and 504 Gateway Timeout mean your request reached a proxy or load balancer but the upstream server didn't respond correctly — network problem, not an API bug. 503 Service Unavailable often means deliberate maintenance or overload.

Response Headers Worth Reading

Content-Type: application/json; charset=utf-8
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1750641600
Retry-After: 3600
Location: /api/v2/users/9183
WWW-Authenticate: Bearer realm="api", error="invalid_token"

When a 401 comes back, WWW-Authenticate tells you exactly what kind of credentials the server expects and sometimes why it rejected yours — error="expired_token" is much more useful than just the status code. Location on a 201 gives you the canonical URL of the newly created resource. Rate limit headers (X-RateLimit-* or the standardized RateLimit-* from the IETF draft) tell you how close you are to being throttled before you actually hit a 429. Build your test suites to read these instead of hammering until you get blocked.

Putting It Together: The Debugging Loop

When an API call misbehaves, work outward from the request line. Verify method and path first — a wrong verb or a transposed path segment accounts for a surprising number of failures. Then check headers: does Authorization have the right token, decoded and unexpired? Does Content-Type match the body format? Then inspect the body itself. Then read the response status and headers before touching the body — the status code and headers often contain the full explanation before you even need to parse JSON.

The HTTP request is not a black box. It's a structured, documented, inspectable protocol. Every field has a defined meaning, and when something breaks, the protocol itself usually tells you why — if you know where to look.