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

§5.5Practical Example: Building an HTTP3 Client and Verifying Frame Order

RFC 9114Runnable lab

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.

ServerClient (aioquic H3)after the QUIC handshake (ALPN h3),each side opens its H3 setup streamsthe request rides a client bidirectionalstreamverified in the trace — SETTINGSfirst on control, HEADERS beforeDATA on stream 0control stream (0x00): SETTINGS(first frame)QPACK encoder (0x02) anddecoder (0x03) streamscontrol (0x00): SETTINGS ·QPACK streamsstream 0: HEADERS (GET /order)+ FINstream 0: HEADERS (200)stream 0: DATA (body) + FIN
Fig. 5.5-1The lab's exchange. Both endpoints open their control and QPACK setup streams (SETTINGS first on the control stream), then a single GET rides one bidirectional stream as HEADERS followed by the response's HEADERS and DATA.RFC 9114 §6.2, §4.1

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:

labs/05-5-http3-client/h3_client.pyPYTHON
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 #

captured output — uv run labs/05-5-http3-client/h3_client.pyTEXT
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).

Note

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.