# flare.http3.client

> A module of flare_mlake's API.

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`

```mojo
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__`

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

### `Http3ClientConnection`

```mojo
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__`

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

#### `open_streams`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
def is_established(self) -> Bool
```

#### `is_usable`

```mojo
def is_usable(self) -> Bool
```

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

#### `close`

```mojo
def close(mut self)
```

#### `retire`

```mojo
def retire(mut self)
```

Graceful CONNECTION_CLOSE, for a connection a pool drops.

## Functions

### `is_idempotent_method`

```mojo
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.
