Learning objective. Learn HTTP/3's uniform frame format and the small catalog of frame types, which stream each rides on, and the transport consequences of framing over QUIC streams rather than over packets.
One frame shape #
Every HTTP/3 frame has the same three-part shape: a variable-length Type, a variable-length Length, and a Frame Payload of exactly that many bytes ([RFC 9114 §7.1]).
That uniformity is deliberate. Because Length delimits the payload regardless of type, a
receiver can skip a frame it does not understand (read Type, read Length, advance Length
bytes), which is what makes HTTP/3 extensible and what GREASE exercises (§1.3).
The whole catalog is small (constants.md §10):
| Type | Frame | Rides on |
|---|---|---|
0x00 |
DATA | request / response / push streams |
0x01 |
HEADERS | request / response / push streams |
0x04 |
SETTINGS | control stream (first frame) |
0x07 |
GOAWAY | control stream |
0x0d |
MAX_PUSH_ID | control stream |
0x03 |
CANCEL_PUSH | control stream |
0x05 |
PUSH_PROMISE | request stream (server → client) |
Which frames live where #
The organizing principle is that a frame's type is legal only on certain streams (§1.3). Three groupings cover it:
- Request/response streams carry a message as HEADERS (the header section), then zero or more DATA frames (the body), then optionally a trailing HEADERS (trailers) ([RFC 9114 §4.1]). A client sends exactly one request per stream; the server sends any interim responses and one final response on the same stream.
- The control stream (one per direction, unidirectional) carries connection-wide frames, and its first frame MUST be SETTINGS. GOAWAY, MAX_PUSH_ID, and CANCEL_PUSH manage shutdown and server push.
- Push streams carry a pushed response (HEADERS + DATA), announced by a PUSH_PROMISE that the server sends on the request stream.
Seen as bytes, the groups differ almost entirely in the payload. The two request/response frames wrap either raw body bytes or a QPACK field section:
The control-stream frames are tiny by comparison: a couple of varints each. SETTINGS is a run of Identifier/Value pairs; GOAWAY and MAX_PUSH_ID each carry one varint identifier:
Order is enforced. An invalid sequence (a DATA frame before any HEADERS, or any frame after
the trailing HEADERS) is a connection error H3_FRAME_UNEXPECTED (0x0105)
([RFC 9114 §4.1], and §5.4).
Framing over streams, not packets #
The transport implication that trips people up is exactly how HTTP/3 frames relate to QUIC
packets. An HTTP/3 frame is a run of bytes inside a QUIC stream, delimited by its own
Length field. Those stream bytes are, of course, carried in QUIC packets, packed into
QUIC STREAM frames (§1.4). What is decoupled is the HTTP/3 frame boundaries
from the packet boundaries, not the bytes from the packets: where one HTTP/3
frame ends is decided by its Length, not by where a packet happens to end. So a single large
DATA frame can span dozens of packets, and one packet's STREAM frame can carry the tail of one
HTTP/3 frame and the start of the next. The two framings never align, and never need to —
QUIC reassembles each stream into one continuous byte sequence, and HTTP/3 reads its frames
out of that sequence by Length.
Seeing it concretely helps. Figure 5.1-4 is a real GET/response: each HTTP/3 frame — the
client's SETTINGS and request HEADERS, the server's response HEADERS and DATA — rides inside a
QUIC STREAM frame on a specific stream, carried in a 1-RTT packet. The bytes shown are the
actual encoding (the request HEADERS is the GET /index.html from §1.3); the
response's d9 is :status: 200 and f5 is content-type: text/plain, both single indexed
bytes from the QPACK static table.
This is also why DATA frames are nearly free: their payload is the body bytes, with only a Type and Length of overhead per frame, and an implementation can emit one big DATA frame rather than many small ones.
"Frame" is overloaded across the stack. A QUIC frame (STREAM, ACK, CRYPTO, …) is a unit inside a QUIC packet (§1.4); an HTTP/3 frame (DATA, HEADERS, …) is a unit inside a QUIC stream. HTTP/3 frames are carried by QUIC STREAM frames, but the two layers' boundaries are unrelated. When a trace tool shows "frames," check which layer it means.
editorial Two things to watch in a capture. First, don't
expect HTTP/3 frame boundaries to line up with packets: a decoder that assumes they do will
mis-parse under any real packetization. Second, an endpoint that receives an unknown frame
type on a stream where extension frames are allowed must skip it, not error; if your
implementation rejects GREASE frames (0x1f * N + 0x21), it will fail interop against peers
that emit them deliberately (§13.2).
Takeaways #
HTTP/3 uses one Type-Length-Payload frame shape for a small set of types, each legal only on particular streams, with strict per-stream ordering. Frames are delimited by their Length within a stream's byte flow, independent of QUIC packets, so extension frames are skippable and DATA is cheap. The richest payload is HEADERS, whose QPACK encoding and its operational constraints are the subject of §5.2.