Skip to main content

Crate h3wire

Crate h3wire 

Source
Expand description

Sans-I/O HTTP/3 (RFC 9114) and QPACK (RFC 9204) engine.

A Connection is the HTTP/3 layer of one QUIC connection. It does no I/O, owns no QUIC stream and keeps no clock: the caller (an adapter over a QUIC stack) feeds it the bytes each stream received, writes out the bytes it asks to send, and executes the Actions it emits. The application learns what happened from Events.

v0.1 uses the QPACK static table only (it leaves the dynamic table capacity and blocked streams settings at their default of 0) and never enables server push.

§Driving a connection

Repeat until nothing moves:

  1. Actions. Execute every Connection::poll_action on the QUIC connection. After Action::OpenUni, open a unidirectional stream and hand its id to Connection::bind_uni.
  2. Send. Collect Connection::sendable first (writing changes it), then for each stream offer Connection::poll_send to the transport and report what it took with Connection::sent; keep writing a stream until the transport accepts less than offered. Bodies never go through the core: Connection::send_data hands out a DATA frame prefix, the caller writes prefix then payload (writev) and reports progress with Connection::data_written.
  3. Receive. Feed received bytes with Connection::recv (see its progress contract): each call consumes a prefix and the caller re-feeds the rest.
  4. Events. Handle every Connection::poll_event; read a header block with Connection::headers and Connection::release it when done.

Caller contracts:

  • FIN is only ever set when the core emits Action::FinishStream; never finish a stream on your own.
  • Errors. An Err from any call is a state notification only. Action::CloseConnection is the sole trigger for the wire effects of a connection error; after it (or Connection::transport_closed) keep draining Connection::poll_event and Connection::poll_action (Connection::peer_settings also keeps working); every other call returns Err(Closed(code)) or does nothing.
  • Critical streams. Read the peer’s control and QPACK streams eagerly, regardless of application demand; otherwise SETTINGS could stall behind a paused request stream. QUIC flow-control credit is the adapter’s job; the core only tracks what it consumed.
  • Stream events. While the connection is open, every request stream gets exactly one terminal event, Event::Finished or Event::StreamAborted. A server can get StreamAborted for a request it never saw Headers on (rejected at a GOAWAY cutoff, malformed, or reset by the peer before any byte arrived). Event::Closed stands in for every stream still open.
  • Datagrams. Connection::parse_datagram only routes; the caller decides whether to buffer Datagram::NotYetOpen payloads briefly, and which requests carry datagram semantics (abort the others with H3Code::DATAGRAM_ERROR).

§Example

A client and a server joined by an in-memory transport that accepts every write:

use h3wire::{Action, Config, Connection, Event, FieldRef, Recv, Role, StreamId};
use std::collections::BTreeMap;

/// Bytes received and not yet consumed, and whether FIN followed them, per stream.
type Inbox = BTreeMap<StreamId, (Vec<u8>, bool)>;

/// One pass of the driving loop; `out` is the peer's inbox. Returns whether anything moved.
fn step(c: &mut Connection, inbox: &mut Inbox, out: &mut Inbox, next_uni: &mut u64,
        body: &mut Vec<u8>) -> bool {
    let mut moved = false;
    while let Some(a) = c.poll_action() {
        moved = true;
        match a {
            Action::OpenUni(kind) => {
                c.bind_uni(kind, StreamId(*next_uni)).unwrap();
                *next_uni += 4;
            }
            Action::FinishStream(s) => out.entry(s).or_default().1 = true,
            a => panic!("not expected in this exchange: {a:?}"),
        }
    }
    for s in c.sendable().collect::<Vec<_>>() {
        let bytes = c.poll_send(s).unwrap();
        let n = bytes.len(); // a real transport may accept fewer
        out.entry(s).or_default().0.extend_from_slice(bytes);
        c.sent(s, n).unwrap();
        moved = true;
    }
    inbox.retain(|&s, (buf, fin)| loop {
        if buf.is_empty() && !*fin {
            return true;
        }
        let n = match c.recv(s, buf, *fin).unwrap() {
            Recv::Paused => return true, // feed again after `release`
            Recv::Body { consumed, range } => {
                body.extend_from_slice(&buf[range]);
                consumed
            }
            Recv::Consumed(n)
            | Recv::Frame { consumed: n, .. }
            | Recv::Raw { consumed: n, .. } => n,
        };
        moved = true;
        let ended = *fin && n == buf.len(); // FIN counts once everything is consumed
        buf.drain(..n);
        if ended {
            return false;
        }
    });
    moved
}

let f = |n: &'static str, v: &'static str| FieldRef::new(n.as_bytes(), v.as_bytes());
let mut client = Connection::new(Role::Client, Config::default());
let mut server = Connection::new(Role::Server, Config::default());
let (mut to_client, mut to_server) = (Inbox::new(), Inbox::new());
let (mut client_uni, mut server_uni) = (2, 3);
let (mut client_body, mut server_body) = (Vec::new(), Vec::new());
let request = [
    f(":method", "GET"),
    f(":scheme", "https"),
    f(":authority", "a"),
    f(":path", "/"),
];
client.send_headers(StreamId(0), &request, true).unwrap();

let (mut respond, mut finished) = (None, false);
loop {
    let mut moved = step(&mut client, &mut to_client, &mut to_server, &mut client_uni,
                         &mut client_body);
    moved |= step(&mut server, &mut to_server, &mut to_client, &mut server_uni,
                  &mut server_body);
    while let Some(e) = server.poll_event() {
        if let Event::Headers { stream, block, .. } = e {
            assert_eq!(server.headers(block).unwrap().pseudo().path, Some(&b"/"[..]));
            server.release(block);
            server.send_headers(stream, &[f(":status", "200")], false).unwrap();
            respond = Some(stream);
        }
    }
    // DATA may start once the HEADERS bytes are written (`Blocked` until then).
    if let Some(s) = respond {
        if let Ok(frame) = server.send_data(s, 5, true) {
            let pipe = &mut to_client.entry(s).or_default().0;
            pipe.extend_from_slice(frame.prefix()); // writev(prefix, payload)
            pipe.extend_from_slice(b"hello");
            server.data_written(s, frame.prefix().len() + 5).unwrap();
            respond = None;
            moved = true;
        }
    }
    while let Some(e) = client.poll_event() {
        match e {
            Event::Headers { block, .. } => client.release(block),
            Event::Finished(s) => finished = s == StreamId(0),
            _ => {}
        }
    }
    if !moved {
        break;
    }
}
assert!(finished);
assert_eq!(client_body, b"hello");

Structs§

Config
Connection configuration. Start from Config::default and set what you need.
Connection
The HTTP/3 state of one QUIC connection. See the crate docs for the driving loop and the caller contracts.
Contexts
Set of stream contexts in which an extension frame is delivered; combine with |.
DataFrame
DATA frame prefix (type 0x00 + length varint) to write before the payload.
FieldRef
A header field, borrowed: what HeaderBlockRef::all and HeaderBlockRef::iter yield and what Connection::send_headers takes.
FrameExtension
An extension frame type the application wants delivered instead of ignored.
H3Code
An HTTP/3 error code (RFC 9114 section 8.1), kept as the raw wire value.
HeaderBlockId
Handle to a received header block (from Event::Headers); stale after Connection::release or connection close.
HeaderBlockRef
A borrowed view of a received header block, from Connection::headers.
PeerSettings
The peer’s SETTINGS, from Connection::peer_settings. Absent settings have their RFC defaults.
Pseudo
Pseudo-header fields of a block, validated before the block was delivered.
StreamId
A QUIC stream id (RFC 9000 section 2.1).

Enums§

AbortSource
Who ended a stream reported by Event::StreamAborted.
Action
A transport operation the caller executes on the QUIC connection; taken with Connection::poll_action, in order.
ConnectionError
The connection is closed: returned by peer-input calls (recv, parse_datagram, stream_reset_received, stop_sending_received).
Datagram
Routing decision for a received HTTP datagram, from Connection::parse_datagram. Ranges index the payload passed in (the part after the Quarter Stream ID).
Event
What happened, for the application; taken with Connection::poll_event.
HeadersKind
Which part of a message a header block is.
Recv
Result of Connection::recv. consumed counts bytes from the start of the caller’s buffer, and every range indexes that buffer (within 0..consumed); payload is never copied.
Role
Which end of the QUIC connection this is.
UniKind
The local unidirectional streams the core opens via Action::OpenUni.
UsageError
A local call the connection refused; it had no effect and no wire effect.