Message anatomy and framing

Request and response structure, start line and headers, Content-Length versus chunked transfer, trailers, and how HTTP/1.1, HTTP/2 and HTTP/3 differ on the wire.

The shape of a message

Every HTTP exchange is two messages with the same skeleton: a start line, header fields, one empty line, and an optional body. Everything else — methods, caching, authentication — is built on that frame.

POST /api/orders?dry=1 HTTP/1.1
Host: api.example.com
Content-Type: application/json; charset=utf-8
Content-Length: 27
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.abc

{"sku":"A-1","qty":2}
HTTP/1.1 201 Created
Location: /api/orders/1042
Content-Type: application/json; charset=utf-8
Content-Length: 62

{"id":1042,"sku":"A-1","qty":2,"status":"pending"}
PartRequestResponse
Start lineMETHOD target VERSIONVERSION STATUS REASON
HeadersRequest metadata, credentials, preferencesResult metadata, caching, cookies
Blank lineTerminates the header sectionTerminates the header section
BodyOptional: JSON, form data, file bytesOptional: the representation or an error
  • Field names are case-insensitive; values are not. Content-Type and content-type are the same field.
  • The target of a request is a path and query, not a full URL, unless the request goes through a forward proxy — then it is absolute.
  • A header field is name: value with no space before the colon. Obsolete line folding (a continuation line) is deprecated and rejected by HTTP/2.

Framing the body

Framing is how the receiver knows where a message ends. HTTP/1.1 has exactly three possibilities, and a message must use only one of them.

Way to frameHeaderUse it when
Known lengthContent-Length: 1234The whole body is available before sending
ChunkedTransfer-Encoding: chunkedThe length is unknown while producing the body
NeitherConnection closes to end the bodyLegacy only — never in new code
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked

1a
{"message":"hello there"}

5
world

0

  • Each chunk is a hexadecimal length, CRLF, the bytes, then CRLF. A chunk of size 0 ends the body.
  • Trailers are header fields sent after the last chunk, announced with Trailer: — useful for a checksum you can only compute once the body is written.
  • HTTP/2 and HTTP/3 remove chunked encoding entirely: frames carry their own lengths, so streaming works without a special header.
⚠️
If a message carries both Content-Length and Transfer-Encoding, different intermediaries may disagree about where the body ends — the basis of request smuggling. Reject such requests at the edge rather than trusting them to be normalised.

HTTP/1.1, HTTP/2 and HTTP/3 on the wire

PropertyHTTP/1.1HTTP/2HTTP/3
TransportTCPTCP (or TLS)QUIC over UDP
Message formatTextBinary framesBinary frames
MultiplexingNone — one request at a time per connectionMany streams on one connectionMany streams, no head-of-line blocking
Header compressionNoneHPACKQPACK
How you get itPlain or TLSALPN negotiation during the TLS handshakeHTTPS Alt-Svc advertisement
Server pushNoDefined, now deprecated in browsersNo
curl -sS -v --http2 https://example.com/ -o /dev/null 2>&1 | grep -iE 'ALPN|using HTTP'
curl -sSI --http3 https://cloudflare.com/ | head -n 1   # needs a curl built with HTTP/3

The version only describes the wire. A request that is slow because the database is slow stays slow in HTTP/3; multiplexing fixes connection-level queuing, not your query plan.

FAQ

Why did my response hang instead of completing?
Almost always a framing mismatch: a Content-Length that does not match the bytes actually written, or a chunked body that never sends the terminating zero chunk. The client waits for bytes that will never arrive.
Do I need to care about headers being text?
Not to write an application, but it matters when debugging: HTTP/2 compresses header names into an index, so a capture tool may show :method and :path pseudo-headers that do not exist in HTTP/1.1.

Proxies, load balancers and connection management Compression, content negotiation and media types

Last refreshed 2026-09-18.