HTTP: The Web's Protocol

You type example.com, hit Enter, and a page appears. In the fraction of a second between those two events, your browser opened a TCP connection to a machine you have never met, sent it a short block of text, and got a short block of text back. That conversation — a request out, a response in — is HTTP, the HyperText Transfer Protocol. Every web page, image, API call, and streaming manifest on Earth rides on it. And the astonishing part, given how much of civilisation now runs on it, is how plain it is: HTTP is human-readable text you could type by hand.

This page is about what that text actually says. We will take a request apart line by line, do the same for a response, meet the handful of methods and status codes you need, and confront the single idea that shapes everything built on top of HTTP — it is stateless: the server remembers nothing about you between one request and the next.

One exchange: request and response

HTTP is a strict request/response protocol. The client always speaks first with a request; the server answers with exactly one response; then that exchange is over. The server never spontaneously sends you anything — it only ever replies. Here is a real request, in full, exactly as it travels down the socket (the blank line is load-bearing — hold that thought):

GET /articles/42 HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 Accept: text/html (blank line)

And the response the server sends back:

HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 34 (blank line) <h1>Article 42</h1><p>Hello!</p>

Both messages share the same four-part shape: a start line, some headers (one Name: value pair per line), a single blank line, and an optional body. The blank line is how the parser knows the headers have ended and the body (if any) begins — it is the frame boundary of the whole format. Everything else is just filling in those four slots.

Anatomy of a request

The request's start line has three whitespace-separated fields — method, path, version — and then the headers do the rest of the talking.

PartExampleWhat it says
MethodGETthe verb — what to do with the resource
Path (target)/articles/42which resource on this server
VersionHTTP/1.1which dialect of the protocol
Hostexample.comwhich website (one IP can host thousands)
User-AgentMozilla/5.0what software is asking
Accepttext/htmlwhich representations the client can use
Body(often empty)data being sent up, e.g. a submitted form

The Host header deserves a spotlight: it is the reason a single server at a single IP address can serve example.com, example.org and ten thousand other sites. The path /articles/42 alone is ambiguous — Host disambiguates which site's /articles/42 you mean. It is mandatory in HTTP/1.1 for exactly this reason.

Anatomy of a response: the status line and status codes

The response's start line is the status line: version, a three-digit status code, and a human-readable reason phrase (200 OK). The code is what your software reads; the phrase is a courtesy for humans. The first digit sorts every code into one of five classes:

ClassMeaningFamiliar members
1xxInformational — "still going"100 Continue, 101 Switching Protocols
2xxSuccess — "here you go"200 OK, 201 Created, 204 No Content
3xxRedirection — "look elsewhere"301 Moved Permanently, 304 Not Modified
4xxClient error — "you messed up"400 Bad Request, 401 Unauthorized, 404 Not Found
5xxServer error — "I messed up"500 Internal Server Error, 503 Service Unavailable

The 4xx/5xx split is a genuinely useful mental model when something breaks: a 4xx says the request itself was wrong (bad URL, missing auth, malformed data) — retrying it unchanged won't help. A 5xx says the request was fine but the server failed — retrying might help once the server recovers. Whole retry and monitoring strategies are built on that one distinction.

The three-digit scheme is inherited from much older text protocols like FTP and SMTP, where a machine needed to branch on a reply without parsing English. The tens and units digit give fine detail, but a dumb client can ignore them and just switch on the first digit: 2 = good, 4 = my fault, 5 = their fault. That is why you can write robust code that handles a status code it has never seen before — 418 I'm a teapot and all — simply by bucketing it as Math.floor(code / 100). The design has aged for fifty years without a rewrite.

The methods, and two words that govern them: safe and idempotent

The method is the verb. Five carry the everyday load, and the specification pins each with two properties that matter enormously for correctness on an unreliable network.

This is not pedantry — it is what lets the network retry. If your connection drops mid-request, a client can safely re-send a GET or PUT without fear of double-acting. It cannot blindly retry a POST that says "charge my card £50", because a second delivery might charge you twice. Idempotency is the property that makes an unreliable network usable.

Parsing a request, for real

Because HTTP is just text with a rigid shape, parsing it is pure string logic — no network needed. The demo below takes a raw request exactly as it would arrive on a socket, splits it at the blank line into headers and body, pulls apart the request line, and builds a structured object. This is, in miniature, what every web server does on every request.

// A raw HTTP request, exactly as bytes arrive on the socket. // \r\n is the real line terminator; a bare blank line ends the headers. const raw = "POST /login HTTP/1.1\r\n" + "Host: example.com\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 27\r\n" + "\r\n" + '{"user":"alice","pw":"hi"}'; // 1) Split headers from body at the FIRST blank line (\r\n\r\n). const sep = raw.indexOf("\r\n\r\n"); const head = raw.slice(0, sep); const body = raw.slice(sep + 4); // 2) The first line is the request line: METHOD PATH VERSION. const lines = head.split("\r\n"); const [method, path, version] = lines[0].split(" "); // 3) Every remaining line is a "Name: value" header. const headers = {}; for (let i = 1; i < lines.length; i++) { const colon = lines[i].indexOf(":"); const name = lines[i].slice(0, colon).trim().toLowerCase(); const value = lines[i].slice(colon + 1).trim(); headers[name] = value; } console.log("method :", method); console.log("path :", path); console.log("version:", version); console.log("host :", headers["host"]); console.log("type :", headers["content-type"]); // 4) Content-Length tells us how many body bytes to expect. const declared = Number(headers["content-length"]); console.log(`body : ${body} (declared ${declared}, actual ${body.length})`); console.log(declared === body.length ? "OK: body fully received" : "WAIT: more bytes to come");

Notice how Content-Length earns its keep: since the underlying stream has no message boundaries, the header is how the receiver knows when it has read the whole body and can stop waiting. Without it (or its cousin Transfer-Encoding: chunked) the server could not tell "the body is done" from "the network is just slow".

Statelessness, and keeping the connection warm

Here is the defining property of HTTP: it is stateless. Each request/response pair is an island. The server processes your request, sends the reply, and forgets you completely. Your next request arrives as if from a total stranger — the server holds no memory of the last one. Everything the server needs to handle a request must travel inside that request.

This sounds like a limitation, and it is the reason cookies and sessions had to be invented. But statelessness is also HTTP's superpower: because no request depends on server memory of a previous one, any server in a fleet can handle any request. That is what makes the web horizontally scalable — you can put a hundred identical servers behind a load balancer and it simply works.

Statelessness is often confused with connectionlessness, but they are different layers. Early HTTP/1.0 did open a fresh TCP connection for every single request and slam it shut after the response — one file, one connection, one handshake. On a page with 50 images that meant 50 handshakes, a wretched waste. HTTP/1.1 made persistent connections (aka keep-alive) the default: the TCP connection stays open and many request/response pairs flow over it, one after another. The protocol is still stateless — the server remembers nothing between requests — but the expensive TCP handshake is paid once and amortised over many exchanges.

The commonest beginner mistake is to imagine the server "knows who you are" the way a shop assistant remembers a customer who is standing in front of them. It does not. When you are logged into a site and click three links, those are three unrelated requests; the server has no built-in thread tying them to one user. If it appears to remember you, that is only because your browser attached a cookie (or a token) to every request, carrying an identifier the server can look up. Pull that cookie out and the server treats each click as a fresh anonymous stranger. Nothing about "being logged in" lives in the connection or the protocol — the state is smuggled along inside every single request, precisely because HTTP itself keeps none.

"Safe" in HTTP has a narrow technical meaning — no server-side side effects — and it says nothing about security or privacy. Two real traps follow. First, a GET is only safe if your handler keeps it read-only; a route like GET /account/delete?id=5 that actually deletes something violates the contract, and web crawlers (which fire GETs freely) will merrily wipe your data. Put state-changing actions behind POST/PUT/DELETE. Second, GET parameters ride in the URL, so they land in browser history, server logs and Referer headers — never put a password or token in a query string. "Safe" means side-effect-free, not confidential.