Learning objective. Build a minimal HTTP/3 client, watch the streams and frames it exchanges, and confirm on a real trace the ordering rules this chapter specified — the control stream's SETTINGS first, and HEADERS before DATA on a request.
What an HTTP/3 client actually does #
Once the QUIC handshake completes with ALPN h3, an HTTP/3 endpoint does a fixed bit of
setup before any request: it opens its control stream (unidirectional type 0x00) and
sends SETTINGS as its first frame, and it opens the QPACK encoder and decoder streams
(0x02, 0x03) (§5.1–§5.2). Only then do requests flow on
client-initiated bidirectional streams.
A client in a dozen lines #
With aioquic's H3Connection, the request itself is short. Create the H3 layer over the
established QUIC connection, then send the pseudo-headers and finish the stream:
from aioquic.h3.connection import H3Connection
from aioquic.h3.events import HeadersReceived, DataReceived
def get(self, path: str) -> int:
sid = self._quic.get_next_available_stream_id() # client bidi: 0, 4, 8, …
self._h3.send_headers(sid, [
(b":method", b"GET"), (b":scheme", b"https"),
(b":authority", HOST.encode()), (b":path", path.encode()),
], end_stream=True) # HEADERS + FIN — no body
self.transmit()
return sid
Incoming frames arrive as H3 events: a HeadersReceived for the :status, then
DataReceived for the body. The lab drives this against a loopback server and reads the
result out of the client's qlog.
Verifying the order on the wire #
request: GET /order
response: 200 body=b'hello over HTTP/3\n'
HTTP/3 unidirectional streams the client opened (setup):
stream 2 control <- SETTINGS is the first frame here
stream 6 qpack_encoder
stream 10 qpack_decoder
HTTP/3 frames on the request stream (client qlog):
stream 0 sent HEADERS
stream 0 received HEADERS
stream 0 received DATA
check: HEADERS precedes DATA on the request stream -> OK
Everything the chapter described is visible. The client opened three unidirectional streams
before requesting: the control stream (2) that carries SETTINGS as its first frame, and
the QPACK encoder (6) and decoder (10) streams (§5.2). The request itself
used bidirectional stream 0 (§4.1), which shows exactly the sequence
§5.1 requires: the client's request HEADERS, then the response HEADERS, then the
response DATA — HEADERS strictly before DATA. Reverse that order and a conformant peer would
answer with H3_FRAME_UNEXPECTED (§5.4).
aioquic's qlog surfaces HTTP/3 frames on request streams and the type of each unidirectional stream, but not a discrete event for the SETTINGS frame itself, which is why the lab confirms the control stream was opened (and that SETTINGS rides it first by rule) rather than printing a SETTINGS frame line. Different stacks log at different granularities; knowing what your tool records is part of reading its traces (§11.3).
Takeaways #
Building an HTTP/3 client is mostly letting a library handle the setup streams and QPACK, then sending pseudo-headers on a bidirectional stream. The trace confirms the framing rules directly: control and QPACK streams open first, SETTINGS leads the control stream, and HEADERS precedes DATA on every request. That completes the HTTP/3 framing chapter; Chapter 6 descends into QPACK, the one piece §5.2 deferred: how the dynamic table is built and kept from blocking.