Skip to content

HTTP

the request and answer the web is built on

What it is

HTTP, the Hypertext Transfer Protocol, is the set of rules a browser, an app or a script follows to ask a server for something and receive a reply, and when that conversation runs inside an encrypted TLS connection the same protocol goes by HTTPS. Every exchange has two halves. The client sends a request: a method such as GET or POST, the target it wants, a list of header fields and sometimes a body. The server answers with a three-digit status code, header fields of its own and, usually, a body. Loading a page, fetching an image and one program calling another are all the same exchange repeated.

GET /pricing/ HTTP/1.1
Host: example.com
Accept: text/html

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=600

The specification calls it stateless. Each request has to carry everything the server needs in order to understand it, and the protocol keeps no memory from one exchange to the next. Logins, baskets and sessions are built on top, usually with cookies: a header field the server sets once and the client sends back on every later request.

The vocabulary is small and precise. GET and HEAD are safe, meaning they ask for a representation and are expected to change nothing. PUT and DELETE are idempotent: sending the same one twice leaves the server where sending it once would have. POST promises neither. Status codes are grouped by their first digit, 2xx for success, 3xx for redirection, 4xx for a problem with the request and 5xx for a problem on the server, so a client meeting an unfamiliar code still knows roughly what happened.

That meaning has stayed put while the wire format has been replaced twice. HTTP/1.1 writes messages as lines of text over a TCP connection, and each connection returns its responses one at a time, in order. HTTP/2 keeps the same methods and fields but frames them in binary and interleaves many requests on one connection. HTTP/3 carries the same frames over QUIC, a transport built on UDP, so a lost packet delays only the stream it belonged to. Since 2022 the IETF has published the semantics in one document and each version’s wire format in another, which is why “what does 404 mean” and “how are the bytes laid out” now have separate answers.

HTTP versus HTTPS is therefore no contest between two protocols: HTTPS is HTTP with TLS underneath, which encrypts the exchange and proves the server’s name. WebSocket starts life as an HTTP request and then upgrades the connection into a two-way channel, and gRPC is a convention for calling remote functions that runs over HTTP/2.

Why it matters

Nearly every system that talks to another over the internet already speaks HTTP, so the decision that matters is rarely whether to use it. It is whether to use what it defines or to treat it as a pipe and rebuild the rest on top.

Using it fully pays off because a long chain of software already reads the vocabulary. A client library may retry a GET that timed out; it must leave a POST alone, and so must any proxy in between. A cache can keep a response marked max-age=600 for ten minutes and serve it to the next reader without asking the server. Monitoring counts 5xx answers as errors without being told to. An API that sends every call as a POST to one address and reports failure inside a 200 response gives all of that up, and every client then has to learn that API’s private rules.

The model has edges. Request and answer suit a client that knows what it wants; they suit a server with news to push far less, which is why live updates usually travel over WebSocket, server-sent events or polling.

Plain HTTP on the public internet is a different question with a short answer. Browsers label it as insecure, anyone on the path can read and alter it, and certificates cost nothing, so public traffic belongs on HTTPS. The choice between HTTP/1.1, HTTP/2 and HTTP/3 is mostly a setting on the server or the edge in front of it, and application code stays the same because the meaning of every request is the same.

What it looks like in practice

The clearest case on this site is an API that was moved onto a framework built around HTTP’s own shape. The HTTP API moved from Fiber to Huma v2, which runs on top of Fiber, so each endpoint is declared with input and output structs and the validation comes from the types rather than from checks written by hand. An OpenAPI description falls out of the same declarations, and a CI check compares the routes the server registers against the ones the description advertises. At 649 paths and 760 operations parity is 100%, held there because a route nobody described fails the build. Exactly two endpoints stayed on raw Fiber: the WebSocket ones, where Huma’s request-and-response model does not fit.

The site itself uses content negotiation. Every page is published as HTML and as Markdown, and one way to get the Markdown is to send a request with Accept: text/markdown and let the server choose. The onion address on that page is plain http:// on purpose. Because that address encodes the service’s public key, Tor has already authenticated and encrypted the circuit by the time the first HTTP request travels over it.

HTTP also turns up as a tool in infrastructure code. When a second server was provisioned, both providers’ firewalls were written directly against their REST APIs through a generic HTTP task instead of a vendor collection. That removed a dependency and its version drift, at the cost of writing each request shape by hand. It has a further cost the page names: the generic HTTP task declares no check-mode support, so a dry run of the play that creates the machine skips every task in it, and the rehearsal is run against the firewall play instead.

Where it goes wrong

Most HTTP bugs come from using a word in the vocabulary to mean something other than what the rest of the chain hears.

An error that arrives as a success

An API answers 200 and puts "error": "card declined" in the body. Caches may store it, dashboards count it as a success, and retry logic in the client never fires. Send the status code that fits, 4xx or 5xx, and describe the problem in the body; RFC 9457 defines a JSON shape for that, served as application/problem+json.

A GET request changes state, for example an unsubscribe or delete link in an email. Crawlers, link previews in chat apps, security scanners and browser prefetching all follow links on their own, and each of them can trigger it. Anything that changes data belongs behind POST, PUT or DELETE, with a confirmation page if the link must exist.

A payment taken twice

The client sends a POST, the connection drops before the answer arrives, and the client tries again. The server had already processed the first one. HTTP gives the client no way to tell those cases apart, so the API has to: accept an idempotency key in a header and return the stored result for a repeated key, or let the client choose the new resource’s identifier and create it with PUT.

A form that turns into a GET

A server answers a POST with 301 or 302 to send it elsewhere. For historical reasons the specification lets user agents switch the method to GET on those two codes, and many do, so the body is dropped on the way. When the method has to survive the redirect, use 307 or 308.

The wrong variant from a cache

One URL serves HTML or Markdown, English or Danish, depending on request header fields. A cache that has been told nothing stores whichever variant it saw first and hands it to the next reader. The response has to name the fields it depends on, for example Vary: Accept, so that a cache keeps one copy per variant instead of one per URL.

Further reading

Glossary