Http2ClientConnection
struct Http2ClientConnectionStateful, socket-agnostic HTTP/2 client driver.
A pure state object -- the caller owns I/O. The lifecycle is:
Construct via :meth:
__init__or :meth:with_config. The constructor pre-queues the client connection preface ("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") plus the initial SETTINGS frame, ready to be drained.Call :meth:
drainonce to pull the preface + initial SETTINGS bytes; write them onto the socket.For each request:
a. Allocate a stream id via :meth:
next_stream_id(odd integers starting at 1). b. Call :meth:send_request(sid, ...)to enqueue the HEADERS frame (with optional CONTINUATION) and any DATA frames bounded by current flow-control windows. DATA that exceeds the window is held back; the driver releases more on inbound WINDOW_UPDATE. c. Call :meth:drainto pull outbound bytes; write them.On the inbound side: call :meth:
feed(bytes)with bytes read from the socket. Internally the driver:- decodes frames via :func:
parse_frame - dispatches them through :meth:
Connection.handle_frame(withis_client = Trueso HEADERS / DATA receipt transitions HALF_CLOSED_LOCAL streams to CLOSED instead of HALF_CLOSED_REMOTE) - ACKs SETTINGS / PING as appropriate
- emits WINDOW_UPDATE for received DATA
- on RST_STREAM, surfaces the error code via
:meth:
stream_errorand marks the stream closed
Then call :meth:
drainagain to pull any auto-emitted outbound bytes (SETTINGS ACK, PING ACK, WINDOW_UPDATE).- decodes frames via :func:
Poll :meth:
response_ready(sid); onceTrue, call :meth:take_response(sid)to pop the(status, headers, body)tuple.When done, optionally send a GOAWAY via :meth:
send_goawayand close the socket.
The driver is :class:Movable but not :class:Copyable; the
HPACK dynamic table inside :class:Connection is mutable
state that must not alias across two driver instances.
Implements Defaultable, Movable
- Fields
conn: Connection: The shared frame / HPACK / stream state machinery (:class:flare.http2.state.Connection).conn.is_clientis set toTrueso HEADERS / DATA receipt transitions HALF_CLOSED_LOCAL streams to CLOSED.inbox: List[UInt8]: Inbound byte buffer. Filled by :meth:feed; drained frame-by-frame on each :meth:feedcall.outbox: List[UInt8]: Outbound byte buffer. Pre-loaded with preface + initial SETTINGS at construction; appended-to by every method that emits a frame. Drained by :meth:drain.greeted: Bool:Trueonce the preface + initial SETTINGS have been queued in :attr:outbox. Mirrors :attr:Http2Connection.greeted.config: Http2ClientConfig: The :class:Http2ClientConfigthe driver was constructed with. Kept on the driver so the high-level :class:Http2Clientfacade can re-read individual fields per-stream (e.g. themax_header_list_sizecap when building outbound HEADERS) without threading it through every per-frame call site.
__init__
def __init__(out self) Default-construct with :class:Http2ClientConfig defaults.
from_h2c_upgrade
def from_h2c_upgrade(var config: Http2ClientConfig) -> Self Build a client-side driver pre-seeded for an h2c upgrade.
Per RFC 7540 §3.2 ("Starting HTTP/2 for HTTP URIs"), after the
server accepts the upgrade with 101 Switching Protocols
the original HTTP/1.1 request implicitly becomes stream id 1
in HALF_CLOSED_LOCAL state from the client's perspective: the
request body has already been delivered on the h1 wire, so
the client sends no further bytes on stream 1; the server
will respond on stream 1 with HEADERS + DATA.
After construction the caller MUST:
- Drain the connection preface + SETTINGS frame from
:attr:
outboxand write it to the (now-h2) socket as the client's connection preface (RFC 9113 §3.5); - Feed inbound bytes into :meth:
feed, expecting the server's initial SETTINGS frame followed by a HEADERS+DATA response on stream 1; - Poll :meth:
response_ready(1)and pop the response via :meth:take_response(1).
The next client-initiated stream id starts at 3 (RFC 9113 §5.1.1: odd, monotonically increasing, and stream 1 is already taken).
- Args
config: Same client config the upgrade request advertised via theHTTP2-Settingsheader (the SETTINGS frame in the connection preface MUST agree with the values the upgrade request committed to).
with_config
def with_config(var config: Http2ClientConfig) -> Self Construct a driver whose underlying SETTINGS reflect config.
Validates config first (RFC 9113 / RFC 7541 bounds);
raises if any field is out of range. Same construction
shape as :meth:Http2Connection.with_config.
drain
def drain(mut self) -> List[UInt8] Return all queued outbound bytes and clear the buffer.
Mirrors :meth:Http2Connection.drain for symmetry.
feed
def feed(mut self, data: Span[UInt8]) Push data (bytes from the socket) into the driver.
Decodes any complete frames in :attr:inbox, applies them
via :meth:Connection.handle_frame, and queues any
auto-generated reply frames (SETTINGS ACK, PING ACK,
WINDOW_UPDATE) into :attr:outbox. Per-stream completion
becomes observable via :meth:response_ready after this
call returns.
An oversized frame (RFC 9113 sec 4.2) queues
GOAWAY(FRAME_SIZE_ERROR) and discards the rest of the input
instead of raising; check :attr:conn.goaway_sent and flush
:meth:drain before closing.
Raises Error: On a connection-level protocol violation the frame layer cannot report as a frame (malformed frame header, etc.).
receive
def receive(mut self, data: Span[UInt8]) :meth:feed that also grows the receive windows with the measured bandwidth-delay product: drivers call this.
next_stream_id
def next_stream_id(mut self) -> Int Allocate the next client-initiated stream id.
RFC 9113 §5.1.1: client-initiated stream identifiers must
be odd, MUST be monotonically increasing, and MUST NOT
exceed 2^31 - 1. Once we'd wrap, the connection MUST
be closed gracefully via GOAWAY.
pump_pending_body
def pump_pending_body(mut self, sid: Int) Re-attempt draining a stream's stashed body after a WINDOW_UPDATE opened the send window. No-op when the stream has no pending body.
has_pending_body
def has_pending_body(self, sid: Int) -> Bool True if stream sid has a body remainder still awaiting a WINDOW_UPDATE (RFC 9113 §6.9).
finish_upload
def finish_upload(mut self, sid: Int) Drop unsent request bytes and finish our half after an early reply.
Preserve the received response. A peer reset may already have closed the stream, in which case no further DATA is permitted.
send_request
def send_request(mut self, sid: Int, method: String, scheme: String, authority: String, path: String, extra_headers: List[HpackHeader], body: Span[UInt8]) Encode + queue a request on stream sid.
Builds the pseudo-header block per RFC 9113 §8.1.2.3 in the
required order (:method, :scheme, :authority,
:path) followed by the caller-supplied
extra_headers (already lower-cased; RFC 9113 §8.1.2),
HPACK-encodes the lot via the shared :class:HpackEncoder,
and emits a HEADERS frame.
If body is empty, the HEADERS frame carries
END_STREAM and the stream transitions to
HALF_CLOSED_LOCAL. Otherwise the body is split into one
or more DATA frames bounded by the negotiated
max_frame_size and the connection-level + per-stream
send windows; the last DATA frame carries
END_STREAM. If the windows can't fit the entire body
right now, we emit as much as fits and stash the
remainder for the caller to flush via
:meth:pump_pending_body after WINDOW_UPDATE has
loosened the windows.
- Args
sid: Stream id allocated via :meth:next_stream_id.method: HTTP method (e.g."GET","POST").scheme:"http"or"https".authority:Host-equivalent (RFC 9113 §8.1.2.3.1).path: Request target (RFC 9113 §8.1.2.3.4).extra_headers: Caller-supplied request headers (lowercased name -> value). MUST NOT contain any of the connection-level headers forbidden by RFC 9113 §8.2.2 (connection,transfer-encoding,keep-alive,proxy-connection,upgrade); the high-level facade strips them before calling.body: Request body bytes; may be empty.
send_extended_connect
def send_extended_connect(mut self, sid: Int, scheme: String, authority: String, path: String, protocol: String, extra_headers: List[HpackHeader]) Open an RFC 8441 Extended CONNECT stream.
Emits a HEADERS frame with the Extended-CONNECT pseudo-header
set (:method = CONNECT, :scheme, :authority, :path, :protocol) without END_STREAM. The stream stays in
OPEN state so the caller can pump bidirectional DATA frames
(e.g. WebSocket frames over h2 -- protocol="websocket").
The peer MUST have advertised
SETTINGS_ENABLE_CONNECT_PROTOCOL=1 (RFC 8441 §3) before
the caller invokes this; gate the call on
:meth:peer_supports_extended_connect. The driver does NOT
block here -- callers may pre-stage the frame and the peer
may have already ACKed our SETTINGS by the time the bytes
are flushed.
- Args
sid: Stream id allocated via :meth:next_stream_id.scheme:"http"or"https"per RFC 8441 §4.authority:Host-equivalent.path: Request target.protocol: Tunnelled protocol token, e.g."websocket".extra_headers: Caller-supplied headers (lowercased).
send_data
def send_data(mut self, sid: Int, body: Span[UInt8], end_stream: Bool) Queue body as one or more DATA frames on stream sid.
Used by tunnelling clients (Extended CONNECT bodies, or
chunked POSTs that need to be fed incrementally) where the
caller wants explicit control over END_STREAM placement.
Same flow-control accounting as the body-emission path of
:meth:send_request. Honours the negotiated send windows;
raises if the body wouldn't fit.
peer_supports_extended_connect
def peer_supports_extended_connect(self) -> Bool Return True once the peer has advertised SETTINGS_ENABLE_CONNECT_PROTOCOL = 1 (RFC 8441 §3).
Latches on the first server-side SETTINGS that includes the
bit and never flips back -- RFC 9113 §6.5 forbids tearing
down an already-advertised SETTINGS value once peers have
acted on it. Callers should gate any
:meth:send_extended_connect call on this returning True.
send_goaway
def send_goaway(mut self, last_stream_id: Int, error_code: Int = Int(0)) Queue a GOAWAY frame (RFC 9113 §6.8).
The peer should treat any stream id > last_stream_id
as not-processed; in-flight streams below that id MAY
complete. error_code defaults to 0 (NO_ERROR) for a
clean shutdown; pass an :class:Http2ErrorCode value for
an abnormal close.
response_ready
def response_ready(mut self, sid: Int) -> Bool Return True when the response on stream sid is fully buffered and ready to be popped via :meth:take_response.
A response is "ready" iff its HEADERS block has been completely received (END_HEADERS observed on either HEADERS or trailing CONTINUATION) and either:
- the stream has been ENDed by the peer (END_STREAM seen on HEADERS or DATA), or
- the stream has been RESET by the peer.
stream_error
def stream_error(self, sid: Int) -> Optional[Int] Return the peer-supplied RST_STREAM error code for sid, if any. None means the stream completed cleanly (or has not been reset).
take_response
def take_response(mut self, sid: Int) -> Http2Response Pop and return the :class:Http2Response for stream sid.
The returned :class:Http2Response owns the header list
and body bytes; the entry is removed from
:attr:conn.streams so the per-connection memory is
reclaimed across many sequential requests on one driver.
Raises Error: When sid is unknown, the stream is not yet
ready (use :meth:response_ready to gate), or
the response lacks the required :status
pseudo-header.
goaway_received
def goaway_received(self) -> Bool True once the peer has sent a GOAWAY frame (RFC 9113 §6.8). The high-level facade SHOULD stop opening new streams; in-flight streams MAY complete.
send_request_open
def send_request_open(mut self, sid: Int, method: String, scheme: String, authority: String, path: String, extra_headers: List[HpackHeader]) Open a request stream that stays OPEN for incremental body writes (client-streaming / bidi).
Like :meth:send_request but emits HEADERS without
END_STREAM and never any body, leaving the stream in OPEN
state so the caller pumps request DATA frames via
:meth:send_data and half-closes with an empty
end_stream=True DATA frame when the request body is done.
headers_received
def headers_received(self, sid: Int) -> Bool True once the response HEADERS block on sid is fully received (END_HEADERS observed). Body / trailers may still be arriving.
stream_ended
def stream_ended(self, sid: Int) -> Bool True once the peer has ENDed (END_STREAM) or RESET the stream, i.e. no more response DATA will arrive.
drain_body
def drain_body(mut self, sid: Int) -> List[UInt8] Move out and return the response DATA accumulated on sid so far, clearing the stream's buffer.
Returns an empty list when no new DATA has arrived. Keeps the
stream entry alive (unlike :meth:take_response) so subsequent
DATA frames keep accumulating; this is what bounds memory on a
long streaming response.
enable_response_streaming
def enable_response_streaming(mut self, sid: Int) Return this stream's DATA credit only when drain_body consumes it.
Call before feeding the response. Drain outbound WINDOW_UPDATEs before waiting for more inbound data.
response_headers
def response_headers(self, sid: Int) -> List[HpackHeader] Return a copy of the current header list for sid (initial HEADERS plus any trailing HEADERS the driver has merged in). Read after :meth:stream_ended to pick up gRPC trailers (grpc-status / grpc-message).
discard_stream
def discard_stream(mut self, sid: Int) Drop the stream entry for sid (after the caller has read the trailers), reclaiming its per-connection memory.
initial_response_headers
def initial_response_headers(self, sid: Int) -> List[HpackHeader] response_trailers
def response_trailers(self, sid: Int) -> List[HpackHeader] cancel_stream
def cancel_stream(mut self, sid: Int)