Http2Connection
struct Http2ConnectionSynchronous HTTP/2 driver with separate I/O sides.
A pure state object: the caller drives I/O. It exposes:
- :meth:
feed— push inbound bytes; returns any reply frames (already encoded) that the state machine generated. - :meth:
drain— pull queued outbound frames as bytes. - :meth:
take_completed_streams— pop ids whose request is ready for handler dispatch. - :meth:
take_request— convert one stream into a plainRequest(a :class:flare.http.Request). - :meth:
emit_response— schedule the response frames for a finished handler invocation.
Implements Defaultable, Movable
- Fields
conn: Connectioninbox: List[UInt8]outbox: List[UInt8]greeted: Boolpeer_settings_seen: Bool: True once the peer's first frame, a non-ACK SETTINGS, has arrived.config: Http2Config: The :class:Http2Configthe driver was constructed with. Kept on the driver so the reactor wiring can re-read individual fields per-stream (e.g. themax_header_list_sizecap when applying inbound HEADERS) without threading it through every per-frame call site.pending_body: Dict[Int, List[UInt8]]: Response bytes for a stream that the send window would not take.pending_pos: Dict[Int, Int]: Offset already flushed out of the matchingpending_body.pending_trailers_k: Dict[Int, List[String]]: Trailer names to send once the matchingpending_bodydrains.pending_trailers_v: Dict[Int, List[String]]: Trailer values, parallel topending_trailers_k.
__init__
def __init__(out self) Default-construct with :class:Http2Config defaults.
Equivalent to Http2Connection.with_config(Http2Config());
kept as a separate __init__ so callers (Http2Connection()
in tests + the inline driver) work byte-for-byte without
an explicit config argument.
with_config
def with_config(var config: Http2Config) -> Self Construct an :class:Http2Connection whose underlying :class:Connection SETTINGS are populated from config.
Validates config first (RFC 9113 / RFC 7541 bounds);
raises if any field is out of range. The resulting driver's
first-emitted SETTINGS frame advertises
max_concurrent_streams per the config; later inbound
SETTINGS from the peer can lower the negotiated values per
RFC 9113 §6.5.
The HPACK dynamic-table size budget is propagated to the
decoder. The allow_huffman_decode flag is plumbed
into conn.hpack_decoder.allow_huffman; the
allow_huffman_encode flag into
conn.hpack_encoder.allow_huffman. Both default to
False so the wire format stays byte-identical to the
legacy default unless the user opts in.
from_h2c_upgrade
def from_h2c_upgrade(var config: Http2Config, req: Request, settings_payload: List[UInt8]) -> Self Build a server-side :class:Http2Connection seeded for an h2c upgrade.
Per RFC 7540 §3.2 ("Starting HTTP/2 for HTTP URIs"), the original
HTTP/1.1 request becomes stream id 1 (implicitly half-closed from
the client side), and the HTTP2-Settings header value (the
raw SETTINGS payload, base64url-decoded) is applied to the
connection immediately. The server emits its initial SETTINGS
frame as the server connection preface; the client's connection
preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n + a SETTINGS
frame) still arrives over the same TCP fd and is processed by
:meth:feed as usual.
- Args
config: Server SETTINGS to advertise. Validated by :meth:with_config.req: The original HTTP/1.1 request that triggered the upgrade. Becomes stream id 1 inHALF_CLOSED_REMOTEstate withheaders_complete = data_complete = Trueso the handler dispatch loop picks it up immediately.settings_payload: Raw bytes of theHTTP2-Settingsheader value (base64url-decoded). Format is identical to a SETTINGS frame body (repeated 6-byte(id, value)pairs). Applied directly to the connection state without emitting a SETTINGS_ACK -- the ACK on the wire is reserved for the proper SETTINGS frame the client sends inside its connection preface.
feed
def feed(mut self, data: Span[UInt8]) Push data (bytes from the socket) into the driver.
drain
def drain(mut self) -> List[UInt8] Return all queued outbound bytes and clear the buffer.
The buffer is handed over rather than copied: it holds whatever was framed since the last drain, which for a streaming response is a window's worth of body.
take_completed_streams
def take_completed_streams(self) -> List[Int] Return stream ids whose request is fully buffered.
A stream already dispatched is excluded by Stream.slab_ready:
emit_response moves a fully written one to CLOSED, and
response_started covers a response parked in pending_body
whose stream stays open until the window drains, so a
WINDOW_UPDATE never re-runs the handler (RFC 9113 sec 8.1).
take_reset_streams
def take_reset_streams(mut self) -> List[Int] Pop the list of stream ids reset by the peer since the last call.
Each id corresponds to an inbound RST_STREAM frame
(RFC 9113 §6.4) processed by :class:Connection.handle_frame.
Used by :class:flare.http._h2_conn_handle.Http2ConnHandle to
flip the matching per-stream :class:CancelCell so the
in-flight handler can short-circuit cooperatively. The list
is drained -- a second call returns an empty list unless a
new RST_STREAM has arrived in the meantime.
goaway_received_flag
def goaway_received_flag(self) -> Bool Return True once the peer has sent a GOAWAY frame (RFC 9113 §6.8). The reactor checks this between dispatches to flip the per-stream cancel cells before draining the connection.
take_request
def take_request(mut self, sid: Int) -> Request Convert stream sid into a :class:flare.http.Request.
emit_response
def emit_response(mut self, sid: Int, var resp: ResponseImpl) Encode + queue the response for sid.
The connection's stream state is advanced to CLOSED after
the response is queued, mirroring HTTP/1.1's per-request
lifetime in the server. When resp.trailers is non-empty the
response is framed as HEADERS [+ DATA] + trailing HEADERS with
END_STREAM on the trailing block (the gRPC-over-HTTP/2 shape);
otherwise it frames as a single buffered response.
There are still no streaming-response bodies on h2 here (one buffered DATA frame per stream). Chunked/flow-controlled h2 response bodies come with the streaming reactor wiring.
pump_pending
def pump_pending(mut self) Flush response bytes parked by a closed send window.
Called after every inbound frame: a WINDOW_UPDATE is what unblocks them, and nothing else re-drives the stream.
take_extended_connect_streams
def take_extended_connect_streams(self) -> List[Int] Return stream ids of open Extended CONNECT tunnels awaiting acceptance: :method=CONNECT + :protocol=websocket, headers complete, not yet END_STREAM'd, still OPEN (RFC 8441). The :protocol marker is cleared by :meth:accept_ws_over_h2, so an accepted tunnel is not re-returned.
accept_ws_over_h2
def accept_ws_over_h2(mut self, sid: Int) Accept a WebSocket Extended CONNECT tunnel on sid: emit a :status=200 HEADERS block WITHOUT END_STREAM (RFC 8441 5.2) so the stream stays open for bidirectional WS DATA. Clears the :protocol marker so the tunnel is not re-surfaced.
stream_is_open
def stream_is_open(self, sid: Int) -> Bool True while sid exists and is still OPEN (bidirectional).
The WS-over-h2 reactor path uses this to detect peer teardown
(RST_STREAM / END_STREAM moves the stream off OPEN) so it can run
the sidecar handler's on_close and drop the tunnel.
drain_stream_data
def drain_stream_data(mut self, sid: Int) -> List[UInt8] Move any inbound DATA accumulated for sid out of the stream record (the WS-over-h2 read path pulls client frames from here).
begin_stream_response
def begin_stream_response(mut self, sid: Int, var resp: ResponseImpl) Queue the leading HEADERS of an incremental streaming response.
Filters hop-by-hop fields (RFC 9113 8.2.2) and encodes a
no-END_STREAM HEADERS block into the outbox. The stream is left
open; :meth:queue_stream_data sends the body and
:meth:end_stream_response closes it (trailers captured by the
caller from resp.trailers before this move).
queue_parked_body
def queue_parked_body(mut self, sid: Int, body: Span[UInt8], pos: Int) -> Int Frame as much of body[pos:] as the send windows will take.
The point of it is what it does not copy. A body too large for the
window is parked and re-pumped on every WINDOW_UPDATE, so handing the
whole remainder over each time and letting queue_stream_data use a
window's worth of it is quadratic in the body: at a 64 KiB window a
28 MiB response copies about 6 GB to send 28 MB. Asking the window
first makes each pump cost a window, not a body.
Returns the bytes consumed, which the caller adds to its own offset.
Both pump paths — a buffered response here and a streaming one in
Http2ConnHandle — go through this rather than repeating it.
queue_stream_data
def queue_stream_data(mut self, sid: Int, data: Span[UInt8]) -> Int Frame as much of data as the send windows allow.
Emits DATA frames (each <= max_frame_size) bounded by the
min of the connection and per-stream send windows, decrements
both, and returns the number of bytes consumed. Returns 0 when
the window is exhausted -- the caller stashes the remainder and
re-pumps on the next WINDOW_UPDATE.
end_stream_response
def end_stream_response(mut self, sid: Int, trailers_k: List[String], trailers_v: List[String]) Close a streaming response.
Emits trailing HEADERS with END_STREAM when trailers are present, otherwise an empty DATA frame with END_STREAM, and advances the stream to CLOSED.