Skip to content

5.6.1 — HTTP: Messages, Methods, Status Codes & Versions

The first version of HTTP, in 1991, was one line long:

GET /index.html

No headers, no version number, no status code. The server sent back an HTML file and closed the connection. That was the entire protocol, and Tim Berners-Lee documented it on a single page.

Everything since has been additions, and the additions are all responses to specific problems. That is the useful way to read this chapter: each HTTP version exists because the previous one had a bottleneck that could be named precisely.

Why "hypertext"?

Hypertext is text containing links to other text, so that reading is non-linear — you follow a reference instead of turning to the next page. Ted Nelson coined the word in 1965. HTTP is the Hypertext Transfer Protocol because it was built to move exactly that: documents whose defining feature is that they point at other documents.

The name has outlived its accuracy. HTTP now carries JSON, video, binary uploads and remote procedure calls, most of which contain no links at all. It is one of many places in computing where the name records what something was for rather than what it does — Chapter 5.1's "packet" is another.

2. What a message actually looks like

HTTP is text-based in versions 1.0 and 1.1, which means you can read it directly and type it by hand. That decision — human-readable over compact — is a large part of why HTTP won over more efficient contemporaries.

A request:

http
POST /api/orders HTTP/1.1                    ← (1) request line
Host: shop.example.com                       ← (2) headers
Content-Type: application/json
Content-Length: 42
Authorization: Bearer eyJhbGci...
                                             ← (3) one blank line
{"sku":"BOOK-042","quantity":2}              ← (4) body
  1. Method, path, version. Three words separated by spaces.
  2. Headers as Name: value pairs, one per line. Names are case-insensitive.
  3. A blank line marks the end of the headers. This is the framing (Chapter 5.4.1) — the parser reads lines until it hits an empty one, and then it knows the body begins.
  4. The body, whose length is given by Content-Length.

A response:

http
HTTP/1.1 201 Created                         ← (1) status line
Content-Type: application/json
Content-Length: 58
Location: /api/orders/8891                   ← (2)

{"id":8891,"sku":"BOOK-042","status":"pending"}
  1. Version, status code, reason phrase. The reason phrase is decorative — no client should parse it, and HTTP/2 removed it entirely.
  2. Headers can carry meaning specific to the status, and Location on a 201 tells the client where the created thing lives.

The Host header is more important than it looks. HTTP/1.0 had no such header, which meant one IP address could serve exactly one website — the server had no way to know which site was wanted, because the path alone does not say. Adding Host in HTTP/1.1 enabled virtual hosting: thousands of sites on one address, distinguished by a header. Essentially all shared hosting, and every reverse proxy routing rule (Chapter 9.9.7), depends on it.

It also creates a chicken-and-egg problem with encryption, covered in Chapter 5.7: the Host header is inside the encrypted body, but the server must choose a certificate before decrypting. The fix is SNI, and it is why HTTPS virtual hosting needed a TLS extension.

3. Methods, and the two properties that actually matter

Nine methods exist. Two properties matter far more than the list.

Safe means the request does not change anything on the server. GET, HEAD and OPTIONS are safe. A safe request may be repeated, prefetched, or issued speculatively by a browser with no consequence.

Idempotent means doing it N times has the same effect as doing it once. GET, HEAD, PUT, DELETE and OPTIONS are idempotent; POST and PATCH are not.

Note that idempotent does not mean "returns the same response". DELETE /orders/42 returns 204 the first time and 404 the second — different responses, same resulting state, which is what idempotency is about.

MethodSafeIdempotentBodyTypical use
GETyesyesnoretrieve
HEADyesyesnoheaders only, no body
OPTIONSyesyesnowhat is allowed, CORS preflight
PUTnoyesyesreplace at a known URL
DELETEnoyesnoremove
POSTnonoyescreate, or anything else
PATCHnonoyespartial update

Why these properties are not academic. Every retry decision in a distributed system depends on them (Chapter 10.9). If a request times out, you do not know whether the server processed it. Retrying an idempotent request is free; retrying a non-idempotent one may charge a card twice. That is why POST endpoints handling money need explicit idempotency keys (Chapter 9.6.3), and why a load balancer will retry a GET to another backend but not a POST.

The incident that made this concrete. In the early 2000s, web applications commonly put "delete" links as ordinary <a href> tags — a GET. Then Google Web Accelerator shipped, which prefetched links to make browsing feel faster. It duly followed every delete link on every page, and people watched their data disappear. The protocol had said for years that GET must be safe; the applications had ignored it; and a tool that trusted the specification exposed every one of them. Chapter 9.6.1 uses this as its opening example, and it is the best available argument that the semantics are a contract rather than a convention.

4. Status codes, read as five families

The first digit is the category, and knowing the families is worth more than memorising codes.

1xx — informational. Rare. 101 Switching Protocols is the WebSocket upgrade (Chapter 5.8). 103 Early Hints lets a server tell the browser what to start fetching before the real response is ready.

2xx — success. 200 OK. 201 Created with a Location header. 202 Accepted meaning "queued, not done" — the correct answer for asynchronous work. 204 No Content for a successful action with nothing to return. 206 Partial Content for range requests, which is how video seeking and resumable downloads work.

3xx — redirection. The two that matter:

  • 301 Moved Permanently — the browser caches this, sometimes forever, and search engines transfer ranking to the new URL. A wrong 301 is very hard to undo, because you cannot reach the clients that cached it.
  • 302 Found and 307 Temporary Redirect — not cached by default.

The difference between 302 and 307 is a genuine trap: 302 historically caused clients to change a POST into a GET, which most implementations still do. 307 and 308 were added to preserve the method explicitly. If you redirect a form submission with a 302, the body is silently dropped.

304 Not Modified is not really a redirect; it is the caching mechanism from Chapter 5.6.2.

4xx — the client's fault.

  • 400 Bad Request — malformed.
  • 401 Unauthorized actually means unauthenticated — you have not proved who you are. The name is a forty-year-old mistake in the specification and it confuses everyone. It must be accompanied by a WWW-Authenticate header.
  • 403 Forbidden means authenticated but not allowed. This is the one that means "unauthorised" in plain English.
  • 404 Not Found — and note it is also the correct answer when you want to hide existence from someone not permitted to know, since 403 confirms the resource is real.
  • 405 Method Not Allowed, 409 Conflict (a version clash, Chapter 9.6.3), 410 Gone (deliberately deleted, unlike 404's "not here"), 422 Unprocessable Content (syntax fine, semantics wrong), 429 Too Many Requests — which should carry Retry-After (Chapter 9.7.5).

5xx — the server's fault.

  • 500 Internal Server Error — the catch-all, and it should never leak a stack trace (Chapter 9.9.3).
  • 502 Bad Gateway — a proxy got an invalid response from upstream.
  • 503 Service Unavailable — temporarily down or overloaded; carries Retry-After.
  • 504 Gateway Timeout — a proxy waited and gave up.

The 502-versus-504 distinction is genuinely useful when debugging behind a proxy: 502 means the upstream replied with garbage or refused the connection; 504 means it did not reply at all in time. One points at the upstream crashing, the other at it being slow.

5. HTTP/1.0 to 1.1: the connection problem

HTTP/1.0 (1996) opened a new TCP connection for every single request. A page with 30 images meant 30 connections, each paying the three-way handshake and each starting congestion control from a tiny window (Chapter 5.4.3).

HTTP/1.1 (1997) made connections persistent. Connection: keep-alive became the default, so one connection carries many requests. This is the single largest performance change in the protocol's history, and its benefit compounds: you pay the handshake once, and the congestion window has time to grow.

But requests on a connection are strictly serial. Request, full response, request, full response. HTTP/1.1's pipelining attempted to fix this by allowing several requests to be sent before their responses arrived — and it failed in practice, because responses had to come back in the same order. One slow response blocked every response behind it. Proxies implemented it badly, and browsers disabled it.

That is head-of-line blocking at the HTTP layer, distinct from TCP's version in Chapter 5.4.3, and it is the bottleneck HTTP/2 was built to remove.

The browser workaround was six connections per origin, which multiplies the handshake cost by six and forced developers into a whole vocabulary of hacks: sprite sheets combining many images into one file, concatenated JavaScript bundles, inlined CSS, and domain sharding — serving assets from static1.example.com and static2.example.com purely to get more than six connections. Every one of those techniques exists to work around a limit in HTTP/1.1, and several became actively harmful under HTTP/2.

HTTP/1.1 also added chunked transfer encoding. With Transfer-Encoding: chunked, a response is sent as a series of size-prefixed chunks terminated by a zero-length one, so the server can start sending before it knows the total length. This is what streaming a generated response depends on, and it is length-prefixed framing exactly as described in Chapter 5.4.1.

6. HTTP/2: binary, multiplexed, and one thing it got wrong

HTTP/2 (2015) came from Google's SPDY experiment and changed the encoding while keeping the semantics identical. Same methods, same status codes, same headers — a different wire format.

It is binary, not text. Everything is a frame with a length, a type and a stream identifier. Harder to read by hand, unambiguous to parse, and no longer vulnerable to the whitespace and line-ending ambiguities that produced HTTP request smuggling attacks (Chapter 8.5).

Multiplexing is the headline. Many streams share one TCP connection, interleaved at the frame level. Request 2's response can begin before request 1's finishes, in any order. This removes HTTP's head-of-line blocking completely, and it makes domain sharding, sprite sheets and aggressive bundling counterproductive — they now cost more than they save.

Header compression (HPACK) matters more than expected. A typical request carries 500–800 bytes of headers, mostly identical on every request to the same origin — the same cookies, the same user agent, the same accept headers. Over 100 requests that is 80 KB of near-duplicate text. HPACK keeps a shared table of previously seen header fields so a repeated header costs a single index byte. On a header-heavy page this is a larger saving than compressing the bodies.

Server push was the feature that failed. The server could send resources the client had not asked for, guessing they would be needed. It was removed from Chrome in 2022, for two reasons worth knowing: the server cannot tell what the client already has cached, so it frequently pushed bytes that were thrown away; and it was hard to use correctly. 103 Early Hints replaced it — the server tells the client what to fetch and lets the client decide, which respects the cache.

And the thing HTTP/2 got wrong, from Chapter 5.4.3: it multiplexed at the HTTP layer while still running over a single TCP connection. TCP's own head-of-line blocking remains, and by collapsing six connections into one, HTTP/2 made a lossy network worse — one lost packet now stalls every stream instead of one sixth of them. On a poor mobile connection HTTP/2 can lose to HTTP/1.1.

Fixing that required changing the transport, which is HTTP/3.

7. HTTP/3: the same protocol on a different foundation

HTTP/3 (RFC 9114, 2022) is HTTP over QUIC (Chapter 5.4.3). The semantics are unchanged again; the transport is replaced.

What it buys, all inherited from QUIC:

  • Genuinely independent streams — one lost packet blocks only its own stream.
  • One round trip to connect, or zero when resuming, because QUIC merges the transport and TLS handshakes.
  • Connection migration — the session survives moving from Wi-Fi to mobile, because a QUIC connection is identified by a connection ID rather than by the four-tuple.
  • Header compression becomes QPACK, a variant of HPACK redesigned so that out-of-order stream delivery cannot corrupt the shared table.

How a browser discovers HTTP/3. It cannot simply try — the server might not support it. So the server advertises support in an Alt-Svc header on an earlier HTTP/1.1 or HTTP/2 response, and the browser uses HTTP/3 on subsequent visits. There is also an HTTPS DNS record (Chapter 5.5) that can advertise it before the first connection, removing that first round trip.

The version comparison, in one place:

1.123
Encodingtextbinarybinary
TransportTCPTCPQUIC over UDP
Concurrency6 connectionsmultiplexed streamsindependent streams
HTTP head-of-line blockingyesnono
TCP head-of-line blockingpartial (6 conns)yesno
Handshake round trips1 + TLS1 + TLS1, or 0 resumed
Header compressionnoneHPACKQPACK
Survives IP changenonoyes

What the interviewer will push on

"What is the difference between PUT and POST?" PUT is idempotent and targets a known URL that it replaces; POST is not idempotent and typically creates a resource whose URL the server chooses. Then the practical consequence: you can safely retry a PUT after a timeout and you cannot safely retry a POST, which is why payment endpoints need idempotency keys.

"401 or 403?" 401 means unauthenticated despite its name, and must carry WWW-Authenticate. 403 means authenticated but not permitted. Then volunteer the security nuance: returning 404 instead of 403 is sometimes correct, because 403 confirms the resource exists.

"Why was HTTP/1.1 pipelining a failure?" Responses had to return in request order, so one slow response blocked all of them — head-of-line blocking at the HTTP layer. Proxies handled it badly and browsers disabled it. That failure is exactly what HTTP/2's multiplexing fixed.

"Should you still bundle and sprite under HTTP/2?" Largely no — those techniques existed to work around the six-connection limit, and under multiplexing they hurt caching granularity, since changing one small module invalidates the whole bundle. Some bundling still helps for compression ratio and module-loading overhead, so the honest answer is "much less, and for different reasons".

"Why can HTTP/2 be slower than HTTP/1.1?" TCP head-of-line blocking. One lost packet stalls every multiplexed stream, whereas six independent connections localised the damage. This is the question that separates knowing the feature list from knowing the trade.

"What is the Host header for?" Virtual hosting — many sites on one IP address, which HTTP/1.0 could not do. Then the follow-up about HTTPS: the header is encrypted, so the server needs SNI to pick a certificate before it can decrypt.

"301 or 302?" 301 is cached aggressively and sometimes permanently, and transfers search ranking — a wrong one is very difficult to reverse. 302 is temporary, and 302 on a POST will usually turn it into a GET and drop the body, which is what 307 and 308 were added to prevent.

One thing to volunteer: name the Google Web Accelerator incident when discussing GET safety. It turns an abstract rule into a story about real data loss, and it makes the point that HTTP's method semantics are a contract that other people's software will hold you to.

Recall

  • HTTP began as a single line and every version since answers a named bottleneck: 1.0 wasted a connection per request, 1.1 made them persistent but serial, 2 multiplexed at the HTTP layer, 3 replaced the transport.
  • A message is a start line, Name: value headers, a blank line as the framing marker, and a body. The Host header is what made virtual hosting possible.
  • Safe means it changes nothing; idempotent means N times equals once — the two properties every retry decision depends on. POST and PATCH are neither.
  • Status families: 1xx informational, 2xx success, 3xx redirect, 4xx client, 5xx server. 401 means unauthenticated, 403 means unauthorised; 502 means the upstream replied badly, 504 means it did not reply in time.
  • 301 is cached hard and transfers ranking, so a wrong one is nearly irreversible; 302 on a POST usually becomes a GET and drops the body, which is why 307/308 exist.
  • HTTP/1.1 pipelining failed because responses had to return in order, so browsers used six connections, which produced sprites, bundling and domain sharding — all counterproductive under HTTP/2.
  • HTTP/2 is binary with multiplexed streams and HPACK header compression, but still suffers TCP head-of-line blocking, so it can lose to HTTP/1.1 on a lossy link. Server push failed because the server cannot see the client's cache; 103 Early Hints replaced it.
  • HTTP/3 is HTTP over QUIC: independent streams, 1-or-0 round-trip handshake, connection migration, QPACK. Discovered via Alt-Svc or an HTTPS DNS record.

Self-test: Why does a timeout on a POST leave you in a worse position than a timeout on a PUT? · Which status means unauthenticated? · Why did pipelining fail while multiplexing succeeded? · Why did collapsing six connections into one make lossy networks worse? · Why is bundling less useful under HTTP/2? · Why was HTTP/2 server push removed?

Next: 5.6.2 covers the part of HTTP that decides whether a request happens at all — caching headers, conditional requests and ETags, and the compression negotiation that shrinks what does travel.