flare_mlake · API · http3

flare.http3.client

HTTP/3 client connection driver -- QUIC + H3 composition.

Stitches the QUIC client driver (:class:flare.quic.client.QuicClientConnection) to the sans-I/O H3 client codecs (:mod:flare.http3.request_writer + :mod:flare.http3.response_reader) into a request/response engine.

Per RFC 9114 a client:

  1. opens its unidirectional control stream and sends SETTINGS first, plus the (empty, static-only) QPACK encoder/decoder uni-streams (:meth:open_streams),
  2. opens a client-initiated bidirectional stream per request and writes HEADERS [+ DATA] with FIN (:meth:send_request),
  3. reads the response HEADERS [+ DATA] off that same bidi stream until the QUIC FIN (:meth:read_response / :meth:fetch).

Three driving styles are exposed so the engine is usable from a single-threaded loopback test (server + client pumped in lockstep on one thread), from a real network client (the server is remote, so a blocking poll loop is fine), and as a multiplexer that keeps many requests in flight over the one connection:

  • granular -- :meth:open_streams, :meth:send_request, and :meth:read_response (one poll burst into a caller-owned reader, returns whether the response is complete) so a test can interleave server ticks,
  • blocking -- :meth:fetch, which polls until the response completes or a poll budget is exhausted,
  • multiplexed -- :meth:request (register an owned reader), :meth:poll_responses (one burst fanned out across every in-flight stream), and :meth:take_if_complete so any number of concurrent requests can share the single QUIC connection.

Inbound STREAM frames are reassembled in offset order by :class:_StreamReasm before reaching the response reader: a reordered, duplicated, or overlapping STREAM frame (multi-packet responses, retransmits) is buffered and only the contiguous prefix is delivered, so the reader always sees the response bytes in order and FIN fires only once every byte up to the final offset arrived.

Structs

Http3ZeroRttOutcome

struct Http3ZeroRttOutcome

Result of :meth:Http3ClientConnection.fetch_0rtt: the response plus how the request was actually carried.

used_0rtt is True only when the request was 0-RTT-eligible and the server accepted early data; replayed is True when 0-RTT was attempted but the server rejected it and the request completed at 1-RTT instead (transparent to the caller).

Implements Copyable, Movable

Fields
response: Http3Response
used_0rtt: Bool
replayed: Bool

__init__

def __init__(out self, var response: Http3Response, used_0rtt: Bool, replayed: Bool)

Http3ClientConnection

struct Http3ClientConnection

An HTTP/3 client over an established QUIC connection.

Wraps a :class:QuicClientConnection (whose handshake must already be complete -- 1-RTT keys installed) and drives H3 request/response exchanges on it. Multiplexes any number of concurrent requests over the single QUIC connection: each :meth:request opens its own bidi stream and registers a :class:_PendingRequest; :meth:poll_responses demuxes one QUIC burst across every in-flight stream.

Implements Movable, PoolableConn

Fields
quic: QuicClientConnection
streams_opened: Bool
max_field_section_size: UInt64
goaway_id: UInt64: The server's GOAWAY stream id (RFC 9114 sec 5.2): requests on streams at or above it will not be processed. UInt64.MAX until a GOAWAY arrives.
peer_settings: Bool: The server's SETTINGS frame has arrived.
peer_connect_protocol: Bool: The server enables extended CONNECT (RFC 9220 sec 3).

__init__

def __init__(out self, var quic: QuicClientConnection, max_field_section_size: UInt64 = UInt64(65536))

open_streams

def open_streams(mut self, early: Bool = False)

Open the client control + QPACK uni-streams (RFC 9114 §6.2). The control stream carries the mandatory first SETTINGS frame; the QPACK streams are empty in flare's static-table-only mode. Idempotent. When early the streams are emitted at 0-RTT (EarlyData) instead of 1-RTT.

send_request

def send_request(mut self, method: String, scheme: String, authority: String, path: String, headers: List[HpackHeader], body: List[UInt8], early: Bool = False) -> UInt64

Open a fresh bidi stream, write the request HEADERS [+ DATA] with FIN, and return the stream id the response will arrive on. Opens the control/QPACK streams first if not already open. When early the control/QPACK streams and the request ride 0-RTT (EarlyData) packets.

read_response

def read_response(mut self, stream_id: UInt64, mut reader: Http3ResponseReader, timeout_ms: Int = Int(100)) -> Bool

Poll one QUIC burst, route this stream's STREAM chunks into the caller-owned reader through its offset-ordered reassembler, and return whether the response is complete. Chunks for other streams (server control / QPACK uni-streams / other requests) are ignored. This is the external-reader single-stream API; :meth:poll_responses is the multiplexed owned-reader counterpart.

request

def request(mut self, method: String, scheme: String, authority: String, path: String, headers: List[HpackHeader], body: List[UInt8], early: Bool = False) -> UInt64

Multiplexed request: open a bidi stream, write HEADERS [+ DATA] with FIN, register an owned reader, and return the stream id. Any number of requests can be in flight at once over the one QUIC connection; drive them with :meth:poll_responses and collect with :meth:take_if_complete. When early the request is emitted at 0-RTT (EarlyData).

send_request_open

def send_request_open(mut self, method: String, scheme: String, authority: String, path: String, headers: List[HpackHeader], timeout_ms: Int = Int(30000), protocol: String = "") -> UInt64

Send request headers without FIN and register its response reader; a non-empty protocol makes it an extended CONNECT.

send_request_body

def send_request_body(mut self, sid: UInt64, body: List[UInt8], fin: Bool, timeout_ms: Int = Int(30000))

Write bounded DATA frames, pumping peer flow-control updates as needed.

poll_responses

def poll_responses(mut self, timeout_ms: Int = Int(100)) -> Bool

Poll one QUIC burst and fan its STREAM chunks out across every in-flight request registered by :meth:request. Returns whether at least one pending response is now complete or failed. Stream errors are retained for that request's accessors; they do not interrupt delivery to other requests.

take_if_complete

def take_if_complete(mut self, stream_id: UInt64) -> Optional[Http3Response]

If the request on stream_id has a fully assembled response, remove it from the in-flight set and return it; otherwise return None (the request stays registered).

head_ready

def head_ready(mut self, stream_id: UInt64) -> Bool

Whether the response head (status + headers) for a multiplexed request has been parsed yet, so a streaming caller can read :meth:stream_status / :meth:stream_headers before draining the body.

stream_status

def stream_status(mut self, stream_id: UInt64) -> Int

The :status of a multiplexed request (0 until the head is parsed -- check :meth:head_ready).

stream_headers

def stream_headers(mut self, stream_id: UInt64) -> List[HpackHeader]

A copy of the application response headers of a multiplexed request (valid once :meth:head_ready).

poll_body

def poll_body(mut self, stream_id: UInt64, timeout_ms: Int = Int(100)) -> Http3BodyChunk

Streaming body read: poll one QUIC burst (fanning chunks out across every in-flight request so none stall), then move out the body bytes that became available on stream_id without ever buffering the whole body. Returns the new bytes plus whether the stream has finished (FIN). After done is True the trailers / final response are still retrievable with :meth:take_if_complete. An unknown / already-taken stream id returns an empty, done=True chunk.

fetch

def fetch(mut self, method: String, scheme: String, authority: String, path: String, headers: List[HpackHeader], body: List[UInt8], timeout_ms: Int = Int(100), max_polls: Int = Int(100)) -> Http3Response

Blocking single-request round-trip for the real-network client: send the request, then poll until the response is complete or the poll budget is exhausted. Raises on a budget timeout (the caller falls back to h2/h1). A thin wrapper over :meth:request/:meth:poll_responses.

fetch_0rtt

def fetch_0rtt(mut self, method: String, scheme: String, authority: String, path: String, headers: List[HpackHeader], body: List[UInt8], timeout_ms: Int = Int(100), max_polls: Int = Int(100)) -> Http3ZeroRttOutcome

Idempotent-only 0-RTT request with transparent 1-RTT fallback.

Gates strictly: a request is 0-RTT-eligible only when its method is idempotent (:func:is_idempotent_method -- a replay is safe) AND the connection resumed a session with rustls early keys installed (:meth:flare.quic.client.QuicClientConnection.early_data_ready). A non-idempotent method, or a fresh (unresumed) connection, is carried normally at 1-RTT.

The request is emitted in the first 0-RTT flight (the control/QPACK streams + the request ride EarlyData packets via :meth:QuicClientConnection.send_stream_early). After the handshake, if the server rejected early data (:meth:QuicClientConnection.early_data_accepted is False), :meth:QuicClientConnection.finish_early_data transparently replays the identical flight at 1-RTT on the same stream, the request still completes, and the result reports replayed=True -- the caller never observes a rejected-0-RTT failure. used_0rtt is True only when the server accepted the early data.

0-RTT packets are not wired into loss recovery, so a lost-but-accepted 0-RTT packet is not retransmitted (fine on the lossless loopback this client targets; the upgrade path is the shared-pn loss-recovery registration in :class:QuicClientConnection).

is_established

def is_established(self) -> Bool

is_usable

def is_usable(self) -> Bool

Established and not told to go away: a pool may reuse it for a new request.

close

def close(mut self)

retire

def retire(mut self)

Graceful CONNECTION_CLOSE, for a connection a pool drops.

Functions

is_idempotent_method

def is_idempotent_method(method: String) -> Bool

Whether method is idempotent per RFC 9110 sec 9.2.2 -- safe to send more than once with the same effect.

The 0-RTT replay hazard (RFC 9001 sec 9.2) is exactly that an on-path attacker can re-send the captured early-data flight, so only idempotent requests are ever eligible to ride 0-RTT (:meth:Http3ClientConnection.fetch_0rtt). POST / PATCH / CONNECT are excluded -- a replay could double a side effect.