Skip to content

HTTP Fundamentals

HTTP is the request/response protocol every web client and server speaks. In its original form it is human-readable text: a client writes a few lines to a socket, the server writes a few lines back, and that is the entire protocol.

Open a raw connection and type an HTTP request by hand. This works because HTTP/1.1 is line-based text.

Terminal window
printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' \
| openssl s_client -quiet -connect example.com:443 -servername example.com

Two messages go over the wire. Both have the same three-part structure: a start line, zero or more headers, a blank line, then an optional body.

<start line>
Header-Name: value
Another-Header: value
<optional body>

The blank line is mandatory — it is how the receiver knows the headers ended. Line endings are CRLF (\r\n).

POST /api/users?notify=true HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 41
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Accept: application/json
{"name":"Ada Lovelace","role":"engineer"}

METHOD SP request-target SP HTTP-version

  • Method — the verb. POST here. Case-sensitive, always uppercase.
  • Request target — usually the path plus query string (/api/users?notify=true). Note it does not include the scheme or host; those live in the Host header. The exception is requests to a proxy, which use the absolute form http://example.com/path.
  • VersionHTTP/1.1.

Case-insensitive names, one per line. The ones you will meet constantly:

Header Purpose
Host The hostname requested. Required in HTTP/1.1. It is what lets one IP serve many sites (virtual hosting).
Content-Type The media type of the request body, e.g. application/json, application/x-www-form-urlencoded, multipart/form-data.
Content-Length Body size in bytes. Tells the server where the body ends.
Accept Media types the client will accept in the response, with optional quality weights: Accept: application/json, text/html;q=0.8.
Accept-Encoding Compression the client supports: gzip, br, zstd.
Accept-Language Preferred languages: en-GB, en;q=0.9.
Authorization Credentials. Basic <base64> or Bearer <token>.
User-Agent A free-text string identifying the client. Historically messy; treat it as a hint, never as a security control.
Referer The URL of the page that produced this request. (Misspelled in the original spec and never fixed.)
Cookie Cookies previously set by this origin. See cookies and sessions.
Origin The scheme+host+port that initiated a cross-origin request. Sent by the browser, not settable by page JS.
If-None-Match / If-Modified-Since Conditional request validators. See caching.

Only some methods carry one. GET bodies are technically allowed by the spec but have no defined semantics and are dropped by many intermediaries — never rely on them.

Terminal window
curl -v https://httpbin.org/post \
-H 'Content-Type: application/json' \
-d '{"name":"Ada"}'
HTTP/1.1 201 Created
Date: Sun, 09 Aug 2026 12:04:11 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 58
Location: /api/users/42
Cache-Control: no-store
{"id":42,"name":"Ada Lovelace","role":"engineer"}

HTTP-version SP status-code SP reason-phrase

The reason phrase (Created) is purely decorative — clients must act on the numeric code. HTTP/2 and HTTP/3 drop it entirely.

Header Purpose
Content-Type Media type of the body. Drives how the browser interprets it.
Content-Length Body length in bytes. Omitted when using chunked transfer encoding.
Content-Encoding Compression actually applied: gzip, br.
Location Target URL for a 3xx redirect, or the URL of a newly created resource with 201.
Set-Cookie Instructs the client to store a cookie. May appear multiple times.
Cache-Control, ETag, Last-Modified, Expires, Vary Caching directives and validators.
Server Server software. Informational; often removed.
WWW-Authenticate Sent with 401 to say which auth scheme to use: Bearer realm="api".
Allow Methods a resource supports. Sent with 405.
Retry-After Seconds (or an HTTP date) to wait. Sent with 429 and 503.

The method states the client’s intent. Two properties classify them.

  • Safe — the request is read-only; it must not change server state. Crawlers and prefetchers assume this.
  • Idempotent — sending it N times has the same effect as sending it once. This is what makes automatic retries possible.
Method Safe Idempotent Body Typical use
GET Yes Yes No Retrieve a resource.
HEAD Yes Yes No Like GET but response has no body. Check existence, size, or freshness.
OPTIONS Yes Yes No Ask what the server supports. Used by CORS preflight.
POST No No Yes Create a subordinate resource, or any non-idempotent action.
PUT No Yes Yes Replace the resource at this URL with the body.
PATCH No No Yes Apply a partial modification.
DELETE No Yes No Remove the resource.

The reason POST is not idempotent: POST /orders twice creates two orders. PUT /users/42 twice leaves one user in one state. DELETE /users/42 twice leaves the user deleted both times — the second call returning 404 does not break idempotency, which is about server state, not the response.

PATCH is not idempotent in general because a patch body can be relative ({"op":"increment","field":"views"}). A JSON Merge Patch that sets absolute values happens to be idempotent, but the method makes no such promise.

Terminal window
# Cheap existence + size check, no body transferred
curl -I https://example.com/large-file.zip
# Ask a server what it allows
curl -X OPTIONS -i https://api.example.com/users

Three digits. The first digit is the class; act on that when you do not recognise the specific code.

Code Meaning
100 Continue The server accepts the headers; send the body. Triggered by a client sending Expect: 100-continue before a large upload.
101 Switching Protocols Response to an Upgrade request. This is how a WebSocket handshake completes.
Code Meaning
200 OK Success, body contains the result.
201 Created A resource was created. Should include Location.
202 Accepted Accepted for processing but not done yet. Async jobs.
204 No Content Success, and there is deliberately no body. Common for DELETE and PUT.
206 Partial Content Response to a Range request. Video seeking, resumable downloads.
Code Meaning
301 Moved Permanently Permanent. Browsers and search engines cache it aggressively — hard to undo.
302 Found Temporary. Historically clients rewrote the method to GET, which is why 307/308 exist.
303 See Other Fetch the Location with GET. The correct redirect after a form POST.
304 Not Modified Your cached copy is still valid. No body.
307 Temporary Redirect Like 302 but the method and body must be preserved.
308 Permanent Redirect Like 301 but method-preserving.
Code Meaning
400 Bad Request Malformed syntax or failed validation.
401 Unauthorized Not authenticated. Misnamed. Must include WWW-Authenticate.
403 Forbidden Authenticated (or not), but not allowed. Re-authenticating will not help.
404 Not Found No resource at this URL. Also used to hide the existence of resources from unauthorised users.
405 Method Not Allowed URL exists, method does not apply. Must include Allow.
409 Conflict State conflict, e.g. a duplicate unique key or an edit collision.
410 Gone Deliberately removed and will not come back.
413 Content Too Large Body exceeds the server’s limit.
415 Unsupported Media Type Wrong Content-Type on the request.
422 Unprocessable Content Syntactically valid but semantically wrong. Widely used for validation failures.
429 Too Many Requests Rate limited. Should include Retry-After.
Code Meaning
500 Internal Server Error Unhandled exception. The catch-all.
501 Not Implemented The server does not support the method at all.
502 Bad Gateway A proxy got an invalid response from upstream.
503 Service Unavailable Temporarily down or overloaded.
504 Gateway Timeout A proxy’s upstream did not answer in time.

HTTP has no built-in concept of a session. Each request is processed as if the server has never seen the client before. The server may keep state internally, but the protocol carries none.

The practical consequence: anything the server must know about you has to travel in every single request. That is what Cookie headers, Authorization headers, and session IDs in URLs all do — they re-establish context on each round trip.

Terminal window
# Request 1 — the server sets a cookie
curl -i -c jar.txt https://httpbin.org/cookies/set/session/abc123
# Request 2 — the cookie is re-sent, restoring context
curl -b jar.txt https://httpbin.org/cookies
# => {"cookies": {"session": "abc123"}}

-c writes a cookie jar, -b reads one. Without the jar, request 2 has no memory of request 1. See cookies, sessions and auth.

The semantics above — methods, status codes, headers — are identical across all three versions. What changes is how bytes are framed and connections are managed.

One request per TCP connection. Connect, request, respond, close. Catastrophically slow: every request pays a full handshake.

Connections stay open by default (Connection: keep-alive is implicit; Connection: close opts out). Multiple requests reuse one connection sequentially.

The remaining problem is head-of-line blocking: responses must come back in request order on a connection. One slow response stalls everything behind it. Browsers work around this by opening ~6 parallel connections per origin, which is why the old advice was to shard assets across subdomains.

HTTP/1.1 also added Host (enabling virtual hosting), chunked transfer encoding, byte ranges, and the caching model.

Terminal window
curl --http1.1 -v https://example.com/ -o /dev/null
> GET / HTTP/1.1
> Host: example.com
< HTTP/1.1 200 OK

HTTP/2 — binary framing and multiplexing

Section titled “HTTP/2 — binary framing and multiplexing”

Standardised in 2015. Same semantics, completely different wire format.

  • Binary framing. Messages are split into frames (HEADERS, DATA, …) instead of text lines.
  • Multiplexing. Many concurrent streams share one TCP connection, interleaved frame by frame. No application-level head-of-line blocking, and no need for 6 connections or domain sharding.
  • Header compression (HPACK). Headers repeat almost identically across requests; HPACK keeps a shared dynamic table so repeats cost a few bytes.
  • Header names are lowercase on the wire — that is why curl prints > user-agent: under HTTP/2.
  • Server push was specified but is now removed from Chrome and effectively dead; use 103 Early Hints or preload instead.

The catch: HTTP/2 still runs over TCP, so a single lost packet stalls every stream on that connection until it is retransmitted. TCP-level head-of-line blocking replaced the HTTP-level kind.

Terminal window
curl --http2 -v https://example.com/ -o /dev/null
# > GET / HTTP/2
# < HTTP/2 200

In practice browsers only use HTTP/2 over TLS, and the version is chosen during the TLS handshake via ALPN.

Standardised in 2022 (RFC 9114). HTTP/3 runs over QUIC, a transport built on UDP that reimplements reliability, ordering and congestion control in userspace, with TLS 1.3 built in rather than layered on.

  • Independent streams. Loss on one stream does not block others — this fixes TCP head-of-line blocking properly.
  • Faster setup. Transport and crypto handshakes are combined: one round trip for a new connection, zero for a resumed one (0-RTT).
  • Connection migration. A connection is identified by a connection ID, not the IP/port tuple, so switching from Wi-Fi to cellular does not break it.
  • QPACK replaces HPACK, adapted for out-of-order stream delivery.

Discovery is via the Alt-Svc response header (or an HTTPS DNS record): the server answers over HTTP/2 and advertises that HTTP/3 is available, and the client upgrades on a later connection.

Terminal window
curl --http3 -v https://cloudflare.com/ -o /dev/null
HTTP/1.1 HTTP/2 HTTP/3
Transport TCP TCP QUIC (UDP)
Framing Text Binary Binary
Concurrency Sequential, ~6 connections Multiplexed streams Multiplexed streams
Head-of-line blocking Application + TCP TCP only None
Header compression None HPACK QPACK
Encryption Optional Optional in spec, required in browsers Always (TLS 1.3)
Terminal window
# Full picture: DNS, TLS, negotiated protocol, request, response
curl -v https://example.com/ -o /dev/null
# Just the response headers
curl -sI https://example.com/
# Follow the whole redirect chain and show every hop
curl -sIL https://example.com/ | grep -E '^(HTTP|location)'
# Time each phase of the request
curl -s -o /dev/null -w 'dns:%{time_namelookup} tcp:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
https://example.com/

The last one is the fastest way to tell whether a slow request is DNS, connection setup, TLS, or the server actually thinking.

  • Every HTTP message is a start line, headers, a blank line, and an optional body.
  • The method declares intent; safe means read-only, idempotent means retry-safe.
  • Status classes matter more than individual codes: 2xx worked, 3xx go elsewhere, 4xx you erred, 5xx the server erred.
  • Host is what makes virtual hosting possible and is mandatory in HTTP/1.1.
  • HTTP is stateless — context must be re-sent on every request.
  • HTTP/1.1, /2 and /3 share identical semantics; they differ only in framing, multiplexing and transport.