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"}| Part | Request | Response |
|---|---|---|
| Start line | METHOD target VERSION | VERSION STATUS REASON |
| Headers | Request metadata, credentials, preferences | Result metadata, caching, cookies |
| Blank line | Terminates the header section | Terminates the header section |
| Body | Optional: JSON, form data, file bytes | Optional: the representation or an error |
- Field names are case-insensitive; values are not.
Content-Typeandcontent-typeare 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: valuewith 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 frame | Header | Use it when |
|---|---|---|
| Known length | Content-Length: 1234 | The whole body is available before sending |
| Chunked | Transfer-Encoding: chunked | The length is unknown while producing the body |
| Neither | Connection closes to end the body | Legacy 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
0ends 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.
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
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transport | TCP | TCP (or TLS) | QUIC over UDP |
| Message format | Text | Binary frames | Binary frames |
| Multiplexing | None — one request at a time per connection | Many streams on one connection | Many streams, no head-of-line blocking |
| Header compression | None | HPACK | QPACK |
| How you get it | Plain or TLS | ALPN negotiation during the TLS handshake | HTTPS Alt-Svc advertisement |
| Server push | No | Defined, now deprecated in browsers | No |
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/3The 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?
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?
:method and :path pseudo-headers that do not exist in HTTP/1.1.Related
Proxies, load balancers and connection management Compression, content negotiation and media types
Last refreshed 2026-09-18.