§5.1
Chapter 05 · HTTP3 Frame Processing and Request Response Semantics

§5.1HTTP3 Frame Types and Their Transport Implications

RFC 9114

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]).

0byte offsetTypetype codevarintLengthpayload bytesvarintFrame Payloadmeaning per Type0..*
Variable-length integer (varint)Length / count prefixVariable-length value
Fig. 5.1-1HTTP/3 frame — one shape for every type (RFC 9114 §7.1)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.

Client → Server
UDP datagram · 1-RTT, just after the handshake
1-RTT packet · short header
QUIC STREAM frame · stream 2carries HTTP/3 SETTINGS: 04 03 01 50 00QUIC STREAM frame · stream 0carries HTTP/3 HEADERS: 01 1e 00 00 d1 d7 … (GET /index.html)
Server → Client
UDP datagram · 1-RTT
1-RTT packet · short header
QUIC STREAM frame · stream 0carries HTTP/3 HEADERS: 01 04 00 00 d9 f5 (:status 200)QUIC STREAM frame · stream 0carries HTTP/3 DATA: 00 03 68 69 0a (body "hi\n")
packets1-RTT
framesSTREAM (app data)
Fig. 5.1-4An HTTP/3 GET on the wire — frames nested in the QUIC flowRFC 9114 §7.2; RFC 9000 §19.8

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.

Note

"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.

In practice

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.