Skip to content

blackbull.client.http2

blackbull.client.http2

HTTP/2 client. HPACK and flow control are connection-owned; response state and deadlines are stream-owned.

ClientResponse dataclass

A complete HTTP response received by the client.

status is the HTTP status code (parsed from the :status pseudo-header). headers are the final head's regular fields as a Headers instance (bytes-keyed, lowercase-indexed). trailers is the trailer section that followed the body: RFC 9113 §8.1 makes it a separate field section, and gRPC puts grpc-status in it, so it is held apart rather than folded into headers where its provenance would be lost. body is the concatenation of all DATA-frame payloads received on the stream. ended_on_head is true when END_STREAM arrived on the final head's HEADERS frame, so no DATA frame and no trailer section followed — gRPC's Trailers-Only shape. It is always false on HTTP/1.1.

HTTP2Client

Bases: HTTP2Connection

Async HTTP/2 client.

Use as an async context manager::

async with HTTP2Client('localhost', 8000) as c:
    res = await c.request(HTTPMethod.GET, '/')

ssl=None (the default) selects plaintext h2c. Provide an ssl.SSLContext with set_alpn_protocols(['h2']) for h2 over TLS.

Multiple request() calls share the same connection: each gets its own odd, monotonically-increasing client-initiated stream ID (RFC 7540 §5.1.1) and the responses are demultiplexed by the receive loop.

frame_factory property

Return this connection's shared HPACK encoder/decoder and header limit.

Every header producer, including extended CONNECT, must reuse this context.

attach_stream_reader(stream_id) async

Deliver stream_id's DATA to a reader from now on, crediting as it is consumed; frames already queued for the raw stream go first.

execute_scenario(scenario) async

Run HTTP/2 fault steps and collect peer/transport outcomes in the result.

Peer and transport failures land on the result; a step outside the vocabulary raises TypeError, an exited client ConnectionError and a scenario the connection cannot run RuntimeError, all before any byte. Abort stops further steps.

receive_raw_frame() async

Escape hatch: read one raw frame, bypassing the receive loop's dispatch.

The read-side twin of send_raw_frame.

Only safe to call when the receive loop is not running (i.e. before __aenter__ finishes or after the loop has been cancelled); a concurrent loop would race this call for the reader.

Inherits client_h2_max_frame_size: one rule, not a second path around it. A scenario needing an over-sized frame opts out with BB_CLIENT_H2_MAX_FRAME_SIZE=0.

Refused after the context exits — the wait for a frame to begin is deliberately unbounded, so on a closed connection this parks for the life of the process.

register_raw_stream(stream_id)

Mark stream_id as a raw-frame stream.

Frames arriving on this stream are pushed into the returned asyncio.Queue instead of being routed through the request/response state machine.

A fresh queue per call: registering the same stream twice is a programming error, not a second subscription. The depth is client_raw_queue_depth, which flow control cannot stand in for.

request(method, path, *, headers=(), body=b'') async

Send one request and await the matching response.

Adds :authority automatically from host:port. Header names and values may be str or bytes; they are normalised to ASCII str for HPACK encoding.

send_raw_frame(frame) async

Escape hatch: write a raw frame to the wire (negative-path tests).

Refused after the context exits: the transport is closed, so the write is discarded by asyncio without a word and the caller is told its frame reached the peer.

unregister_raw_stream(stream_id)

Stop routing frames for stream_id into its raw-frame queue.

The one door a closed client still admits: this is teardown, nothing here needs the connection, and a cleanup path that raises after close turns an orderly shutdown into an error. What the peer still sends on the stream is ignored as after our own reset (§5.1).