flare_mlake · API · http3

flare.http3.server

flare.http3.server -- HTTP/3 server connection driver.

Wraps the sans-I/O HTTP/3 codec primitives (:class:flare.http3.Http3RequestReader, :func:flare.http3.encode_response_headers) and the QUIC connection driver (:class:flare.quic.server.QuicConnection) into a per- connection HTTP/3 server.

Stream layout (RFC 9114 §6)

HTTP/3 over QUIC uses four families of streams:

  • Bidirectional request streams -- one per request/response exchange. The client opens, sends HEADERS + DATA frames; the server replies on the same stream with HEADERS + DATA + (FIN).
  • Unidirectional control stream (type 0x00) -- exactly one per direction; carries SETTINGS first, then GOAWAY / MAX_PUSH_ID over the connection lifetime. The server opens its own as stream 3 with its first 1-RTT flight (:meth:Http3Connection.take_control_stream_start).
  • Unidirectional push stream (type 0x01) -- server-initiated. Not implemented (RFC 9114 deprecates push as of revision 9).
  • Unidirectional QPACK encoder stream (type 0x02) + decoder stream (type 0x03) -- carry dynamic-table instructions. The driver mirrors the peer encoder's inserts into a per-connection dynamic table (RFC 9204) so request field sections referencing dynamic entries decode, and emits Insert Count Increment back on the decoder stream. Dynamic- table use is gated on the qpack_max_table_capacity the SETTINGS advertise: the default (0) keeps static-only behavior.
What ships here
  • :class:Http3Config -- per-connection HTTP/3 config: max field section size, max blocked streams, the GOAWAY threshold above which the server stops accepting new request streams.
  • :class:Http3Connection -- the per-connection driver carrier. Owns the per-stream :class:Http3RequestReader instances (keyed by QUIC stream ID), the per-stream accumulated request state, and the per-stream pending outbound bytes.
  • :class:Http3StreamType -- the unidirectional-stream type codepoints from RFC 9114 §6.2.

Connection errors: an undecodable request field section sets :attr:Http3Connection.connection_error_code to QPACK_DECOMPRESSION_FAILED (0x200), and the driver raises the other H3 / QPACK connection errors with the code's name in the message (:func:h3_error_code maps it back). The QUIC reactor closes the connection with that code.

The driver is sans-I/O: the QUIC reactor feeds reassembled stream chunks in via :meth:Http3Connection.feed_stream_chunk and drains pending outbound frames via :meth:Http3Connection.take_response_frames. The Handler dispatch sits on the QUIC reactor side; the H3 layer surfaces the Request once it's fully reassembled and the reactor calls :meth:Http3Connection.emit_response once the Handler returns.

References:

  • RFC 9114 "HTTP/3".
  • RFC 9204 "QPACK: Field Compression for HTTP/3".

Structs

Http3StreamType

struct Http3StreamType

RFC 9114 §6.2 unidirectional stream types.

Each uni stream's first varint is the stream type; the reader dispatches on this varint to the matching internal state machine (control / push / qpack-enc / qpack-dec).

Implements Movable

comptime CONTROL = Int(0)

RFC 9114 §6.2.1 -- control stream. Carries SETTINGS, GOAWAY, MAX_PUSH_ID. Exactly one per direction.

comptime PUSH = Int(1)

RFC 9114 §6.2.2 -- push stream. Server-initiated. Deprecated as of RFC 9114 revision 9; this codepoint is carried so the reader can reject incoming push streams with H3_STREAM_CREATION_ERROR.

comptime QPACK_ENCODER = Int(2)

RFC 9204 §4.2 -- QPACK encoder stream. Carries dynamic- table insert instructions.

comptime QPACK_DECODER = Int(3)

RFC 9204 §4.2 -- QPACK decoder stream. Carries section-acknowledgement + stream-cancellation instructions.

Http3Config

struct Http3Config

Per-connection HTTP/3 settings.

The server advertises these via the control stream's SETTINGS frame (RFC 9114 §7.2.4). Production defaults match the values the OpenAPI gen + the cookbook examples expect.

Implements Copyable, Defaultable, Movable

Fields
max_field_section_size: UInt64: RFC 9114 §7.2.4.2 -- maximum total size (in bytes) the server will accept for a single field section. Default: 65536. The reader rejects oversize header blocks with H3_EXCESSIVE_LOAD.
qpack_max_table_capacity: UInt64: RFC 9204 §3.2.2 -- maximum bytes the server is willing to spend on the QPACK dynamic table. Default 0 means static- table-only mode; the SETTINGS we emit tell the peer not to send dynamic-table inserts.
qpack_blocked_streams: UInt64: RFC 9204 §3.2.3 -- maximum streams the server is willing to leave blocked on QPACK dynamic-table insertions. Default 0 (paired with qpack_max_table_capacity=0).
enable_connect_protocol: Bool: RFC 9220 §3 -- whether to advertise SETTINGS_ENABLE_CONNECT_PROTOCOL and accept extended CONNECT (:protocol) requests. Default False: advertising it obliges the server to accept such requests (RFC 8441 sec 3), and it used to be advertised while every one was refused.
max_body_size: UInt64: Largest request body buffered for a handler. A request with more is answered 413 and the client asked to stop sending (RFC 9114 sec 4.1.2). Default 10 MiB, as on h2.
stream_request_bodies: Bool: Hand a request with a body to the handler on its HEADERS, the body to follow through Request.body_stream() as it arrives, instead of after the whole body is buffered; credit is granted as the handler reads, so max_body_size does not apply. Default False: handlers otherwise expect the whole body in Request.body.
h3_datagram: Bool: RFC 9297 -- advertise SETTINGS_H3_DATAGRAM and carry HTTP Datagrams on extended CONNECT tunnels. Needs QUIC DATAGRAM support (QuicServerConfig.max_datagram_frame_size > 0). Default False.
goaway_threshold_streams: UInt64: Local soft cap on the stream count above which the server emits GOAWAY and refuses new request streams. Default UINT64_MAX (effectively no cap; mirrors the H2 server's behaviour).

__init__

def __init__(out self)

Http3Connection

struct Http3Connection

Per-connection HTTP/3 server driver.

Owned by the QUIC reactor. One instance per QUIC connection that negotiated the h3 ALPN identifier. The driver:

  • Tracks per-bidirectional-stream :class:_Http3StreamState instances keyed by QUIC stream ID. Each carrier owns the sans-I/O frame reader, the accumulated request bytes / headers / body / trailers, and the pending outbound response bytes.
  • Carries the SETTINGS the server announced + the client announced.
  • Tracks the control-stream lifecycle (whether the peer's SETTINGS arrived yet, whether GOAWAY was emitted).
  • Carries the QPACK state, including a per-connection dynamic table (RFC 9204) mirroring the peer encoder's inserts when qpack_max_table_capacity is advertised non-zero.

The driver is sans-I/O: the QUIC reactor feeds reassembled stream chunks in via :meth:feed_stream_chunk and drains pending outbound frames via :meth:take_response_frames. The Handler dispatch happens on the reactor side -- the H3 layer surfaces the assembled :class:Request via :meth:take_request and accepts the matching :class:Response via :meth:emit_response.

Implements Copyable, Defaultable, Movable

Fields
config: Http3Config: Local configuration -- the values the server advertises via SETTINGS.
peer_settings_received: Bool: Whether the peer's SETTINGS frame has arrived on the control stream yet. Until True, the driver buffers any application traffic that would depend on the negotiated field-section-size + QPACK parameters.
goaway_emitted: Bool: Whether the server has emitted GOAWAY. Once True, request streams at or above :attr:goaway_id are rejected.
goaway_id: UInt64: The stream id the GOAWAY named: requests below it are still served (RFC 9114 sec 5.2), so one reordered behind the GOAWAY is not lost.
streams: Dict[Int, _Http3StreamState]: Per-stream state carriers. Keys are QUIC stream IDs. Streams are removed via :meth:close_request_stream once the response is fully drained from the outbox.
control_stream_id: Int: QUIC stream ID of the locally-opened control stream. -1 until the driver opens the control stream during connection setup.
qpack_encoder_stream_id: Int: QUIC stream ID of the locally-opened QPACK encoder stream. -1 in static-only mode.
qpack_decoder_stream_id: Int: QUIC stream ID of the locally-opened QPACK decoder stream. -1 in static-only mode.
peer_control_stream_id: Int: QUIC stream ID of the peer's control stream once observed. -1 until the peer's uni-stream type prefix (0x00) is read. RFC 9114 §6.2.1: at most one peer control stream per direction; a second control stream is H3_STREAM_CREATION_ERROR.
peer_qpack_encoder_stream_id: Int: RFC 9204 §4.2 -- peer's QPACK encoder uni-stream. -1 until the type prefix (0x02) arrives.
peer_qpack_decoder_stream_id: Int: RFC 9204 §4.2 -- peer's QPACK decoder uni-stream. -1 until the type prefix (0x03) arrives.
peer_settings_max_field_section_size: UInt64: RFC 9114 §7.2.4.2 -- peer-advertised maximum field section size. UINT64_MAX until the peer's SETTINGS frame arrives.
peer_settings_qpack_max_table_capacity: UInt64: RFC 9204 §3.2.2 -- peer-advertised QPACK dynamic-table capacity. 0 until the peer's SETTINGS frame arrives + only non-zero if the peer opts into dynamic-table inserts.
peer_settings_qpack_blocked_streams: UInt64: RFC 9204 §3.2.3 -- peer-advertised blocked-streams budget. 0 until the peer's SETTINGS frame arrives.
peer_settings_enable_connect_protocol: Bool: RFC 9220 §3 -- whether the peer advertises CONNECT- Protocol support.
peer_settings_h3_datagram: Bool: RFC 9297 -- the peer sent SETTINGS_H3_DATAGRAM = 1.
peer_quic_datagram: Bool: Set by the QUIC layer: the peer advertised a non-zero max_datagram_frame_size. SETTINGS_H3_DATAGRAM = 1 without it is an H3_SETTINGS_ERROR (RFC 9297 sec 2.1.1).
peer_goaway_max_stream_id: UInt64: RFC 9114 §5.2 -- peer-emitted GOAWAY identifier. UINT64_MAX means no GOAWAY has arrived yet; once a GOAWAY arrives this holds the maximum stream id the peer will accept. Stream ids >= this value are aborted by the H3 layer.
reassembly: Dict[Int, _StreamReassembly]: Per-stream offset reassembly in front of the readers; see :meth:feed_stream_frame.
closed_request_streams: Dict[Int, Bool]: Client request streams answered and closed above :attr:closed_request_floor. A retransmitted STREAM frame for a stream the server had already answered found no state, opened it afresh and ran the request a second time.
closed_request_floor: Int: Every client bidi stream id below this is closed. Advanced as streams close in order, so the tombstone set stays small.
peer_uni_buffers: Dict[Int, List[UInt8]]: Per-uni-stream inbound buffer. The stream-type varint may span multiple chunks (it's at most 8 bytes but the QUIC layer may split smaller); we buffer until the type is resolvable. Once resolved, the buffer carries the remainder of the stream payload.
peer_uni_kinds: Dict[Int, Int]: Per-uni-stream resolved type: 0 (control), 1 (push), 2 (qpack-enc), 3 (qpack-dec), -1 (unknown grease / reserved). Empty means the type varint has not been resolved yet on that stream.
qpack_table: ArcPointer[QpackDynamicTable]: RFC 9204 QPACK dynamic table mirroring the peer encoder's insert log. Shared (by ArcPointer) with every per-stream reader so a request HEADERS field section can resolve dynamic references the peer streamed over its encoder stream. Capacity is 0 (static-only) unless :attr:config advertises a non-zero qpack_max_table_capacity.
pending_qpack_increment: Int: Count of peer inserts applied since the last Insert Count Increment we emitted on our QPACK decoder stream (RFC 9204 §4.4.3). Drained by :meth:take_qpack_decoder_frames.
control_stream_sent: Bool: Whether :meth:take_control_stream_start has handed the reactor the server's control-stream prefix (H3-07).
connection_error_code: UInt64: Non-zero once the driver has hit an error that RFC 9114 / 9204 make a connection error (so far an undecodable field section, QPACK_DECOMPRESSION_FAILED). The QUIC layer reads it after each feed and closes the connection with this code.
connection_error_reason: String: Human-readable cause of :attr:connection_error_code.

__init__

def __init__(out self)

with_config

def with_config(config: Http3Config) -> Self

Construct with a non-default config carrier.

open_request_stream

def open_request_stream(mut self, stream_id: Int)

Allocate a per-stream carrier for an inbound bidirectional QUIC stream. Idempotent: re-opening the same stream ID is a no-op (the QUIC reactor occasionally fires the open event redundantly when a stream sees both HEADERS and DATA before the dispatcher polls).

close_request_stream

def close_request_stream(mut self, stream_id: Int)

Drop the per-stream carrier. Called after the response FIN has been emitted (so the server doesn't keep parser state around for a stream that's done).

is_closed_request_stream

def is_closed_request_stream(self, stream_id: Int) -> Bool

Whether stream_id is a client request stream the server has already answered and closed.

feed_stream_frame

def feed_stream_frame(mut self, stream_id: Int, offset: UInt64, data: Span[UInt8], fin: Bool)

Feed one QUIC STREAM frame, by offset.

Bytes reach the h3 readers in order, once each; FIN is signalled only when every byte up to the final size has arrived. Frames for a request stream already answered are dropped. Uni streams go to :meth:feed_uni_stream_chunk, bidi streams to :meth:feed_stream_chunk and :meth:signal_end_of_stream.

delivered_offset

def delivered_offset(self, stream_id: Int) -> UInt64

Bytes of stream_id delivered in order so far (0 for a stream not seen).

has_stream

def has_stream(self, stream_id: Int) -> Bool

Whether the driver currently tracks a carrier for this stream ID. Useful for testing the open / close lifecycle without needing the full reactor.

active_request_count

def active_request_count(self) -> Int

Number of active per-stream carriers. The reactor uses this to decide when to emit GOAWAY (above goaway_threshold_streams).

stream_has_headers

def stream_has_headers(self, stream_id: Int) -> Bool

Whether the first HEADERS frame has been fully parsed on stream_id. The reactor checks this to decide when a stream is ready for handler dispatch (HEADERS alone is enough for a GET; DATA + FIN is required for methods with a body).

stream_fin_received

def stream_fin_received(self, stream_id: Int) -> Bool

Whether the QUIC layer has signalled FIN on stream_id. Set via :meth:signal_end_of_stream.

stream_protocol_error

def stream_protocol_error(self, stream_id: Int) -> String

Per-stream protocol-error message; empty when the reader has not hit a protocol error.

feed_stream_chunk

def feed_stream_chunk(mut self, stream_id: Int, var chunk: List[UInt8])

Feed a reassembled stream-data chunk into the per- stream reader.

Idempotently allocates the stream's carrier if it isn't tracked yet (a chunk arriving for an unseen bidi stream is the open-stream signal). Appends the chunk to the per-stream inbox, then drains the inbox one frame at a time via :func:feed_into. Each frame's callback fires through a transient :class:_Http3EventCollector; the collector's contents are merged into the per-stream accumulator after the call returns.

Returns when either:

  • feed_into returns NEEDS_MORE (the inbox doesn't yet hold a complete next frame).
  • The reader transitions to DONE (trailers fired, protocol error, or stream-level shutdown).

signal_end_of_stream

def signal_end_of_stream(mut self, stream_id: Int)

Signal that the QUIC layer observed FIN on stream_id. The reader's sans-I/O frame state has no FIN signal of its own, so the reactor must inform the H3 driver via this method when the last STREAM frame on the request side carries the FIN bit. Used by the reactor to decide when a request-with-body has been fully reassembled.

take_completed_streams

def take_completed_streams(self) -> List[Int]

Return the IDs of streams that have a request ready for handler dispatch.

A stream is ready when:

  • The first HEADERS frame has been fully parsed (headers_complete=True), AND
  • Either the request has no body (FIN observed) or the reader has reached DONE (trailers fired), AND
  • The reactor has not already pulled this request via :meth:take_request, AND
  • No protocol error has fired on the stream.

take_request

def take_request(mut self, stream_id: Int) -> Request

Assemble + return the :class:Request for stream_id. Idempotent guard: subsequent calls on the same stream raise so the reactor can't accidentally dispatch the same request through a Handler twice.

is_tunnel

def is_tunnel(self, stream_id: Int) -> Bool

Whether stream_id carries an extended CONNECT request.

body_backlog

def body_backlog(self, stream_id: Int) -> Int

Body bytes of a streamed request received and not yet read by its handler; -1 when the stream has no body being streamed.

take_stream_body

def take_stream_body(mut self, stream_id: Int) -> List[UInt8]

The DATA payload bytes received on stream_id since the request (or the last call) took them: an extended CONNECT tunnel's inbound bytes.

emit_response

def emit_response(mut self, stream_id: Int, var response: ResponseImpl)

Encode response into the per-stream outbox so the reactor can drain it via :meth:take_response_frames.

Emits HEADERS + (zero or more) DATA frames. Trailers and the FIN bit are handled by the reactor (FIN is a QUIC-level signal); this method only buffers the H3 frame bytes. Idempotent guard: emitting twice for the same stream raises so the reactor can't accidentally write a stale response after the stream is finalized.

feed_uni_stream_chunk

def feed_uni_stream_chunk(mut self, stream_id: Int, var chunk: List[UInt8])

Feed reassembled bytes from a peer-initiated unidirectional QUIC stream into the H3 driver.

The first varint on each uni stream is the stream-type codepoint (RFC 9114 §6.2). Once resolved, the driver dispatches subsequent bytes to the matching state machine:

  • 0x00 (Control) -- parses SETTINGS first, then GOAWAY / MAX_PUSH_ID over the stream's lifetime.
  • 0x01 (Push) -- refused: only a server opens push streams, so a client's is H3_STREAM_CREATION_ERROR (RFC 9114 §6.2.2).
  • 0x02 (QPACK encoder) -- replayed into the connection dynamic table via :meth:_feed_peer_qpack_encoder_stream; each insert owes an Insert Count Increment back on our decoder stream (drained by :meth:take_qpack_decoder_frames).
  • 0x03 (QPACK decoder) -- captured but unused (we emit only static-table field sections, so the peer never acknowledges dynamic inserts to us).

A second QPACK encoder or decoder stream is H3_STREAM_CREATION_ERROR (RFC 9204 §4.2), like a second control stream.

The stream-type varint may span multiple chunks (it is at most 8 bytes per RFC 9000 §16). The driver buffers until the type is resolvable + drains the buffer once resolution succeeds.

take_qpack_decoder_frames

def take_qpack_decoder_frames(mut self) -> List[UInt8]

Drain the QPACK decoder-stream bytes the connection owes the peer: an Insert Count Increment covering every encoder-stream insert applied since the last drain (RFC 9204 section 4.4.3). Returns empty when nothing is pending.

The default config advertises qpack_max_table_capacity = 0, so a conforming peer never streams dynamic inserts and this never has anything to drain. Routing the drained bytes onto a locally-opened QPACK decoder uni-stream is the reactor-side upgrade path, gated on advertising a non-zero capacity. Section Acknowledgment / Stream Cancellation are likewise deferred -- with our static-only encoder the peer never blocks on them.

emit_initial_settings

def emit_initial_settings(self) -> List[UInt8]

Build the server's outbound control-stream prefix: the stream-type varint (0x00) followed by a SETTINGS frame carrying the local :class:Http3Config values.

The reactor sends these bytes as the very first payload of the server's control stream (RFC 9114 §6.2.1: SETTINGS MUST be the first frame); :meth:take_control_stream_start is the once-only form it calls. This is the only frame the server is required to send proactively; GOAWAY / MAX_PUSH_ID are emitted on demand.

take_control_stream_start

def take_control_stream_start(mut self) -> List[UInt8]

The bytes to send at offset 0 of the server's control stream (:data:H3_SERVER_CONTROL_STREAM_ID): stream type 0x00 and the SETTINGS frame (RFC 9114 sections 6.2.1, 7.2.4). Returns them once; every later call returns an empty list. Also records :attr:control_stream_id.

The QUIC reactor calls this with the first 1-RTT flight, so the control stream is initiated at the beginning of the connection (H3-07: it was never opened, and the client never received the server's SETTINGS).

emit_goaway

def emit_goaway(mut self, max_stream_id: UInt64) -> List[UInt8]

Build a GOAWAY frame announcing max_stream_id (RFC 9114 §5.2). The reactor writes the result onto the locally-opened control stream immediately and flips :attr:goaway_emitted so the next open_request_stream rejects with H3_REQUEST_CANCELLED.

take_response_frames

def take_response_frames(mut self, stream_id: Int) -> List[UInt8]

Drain pending outbound bytes for stream_id and return them to the reactor for emission as QUIC stream- data. Returns an empty list when nothing is queued. The reactor calls this repeatedly until it returns empty; each call is destructive (drained bytes are not held for re-reads).

Functions

h3_error_code

def h3_error_code(message: String) -> UInt64

The HTTP/3 / QPACK error code (RFC 9114 section 8.1, RFC 9204 section 6) named by an error message, or :data:H3_GENERAL_PROTOCOL_ERROR when it names none.

The driver raises Error with the code's name in the text ("H3_FRAME_UNEXPECTED: ..."); the QUIC layer maps that to the CONNECTION_CLOSE error code through this function.

Aliases

comptime H3_SERVER_CONTROL_STREAM_ID = Int(3)

QUIC stream id of the server's control stream: the first server-initiated unidirectional stream (RFC 9000 section 2.1, 0b11).

comptime H3_GENERAL_PROTOCOL_ERROR = UInt64(257)

RFC 9114 section 8.1: the default code for an error with no more specific H3 code.

comptime H3_STREAM_CREATION_ERROR = UInt64(259)
comptime H3_CLOSED_CRITICAL_STREAM = UInt64(260)
comptime H3_FRAME_UNEXPECTED = UInt64(261)
comptime H3_FRAME_ERROR = UInt64(262)
comptime H3_EXCESSIVE_LOAD = UInt64(263)
comptime H3_SETTINGS_ERROR = UInt64(265)
comptime H3_MISSING_SETTINGS = UInt64(266)
comptime H3_DATAGRAM_ERROR = UInt64(51)

RFC 9297 sec 2.1: a malformed HTTP Datagram.

comptime QPACK_DECOMPRESSION_FAILED = UInt64(512)

RFC 9204 section 6.

comptime QPACK_ENCODER_STREAM_ERROR = UInt64(513)
comptime QPACK_DECODER_STREAM_ERROR = UInt64(514)