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:
- Actions. Execute every
Connection::poll_actionon the QUIC connection. AfterAction::OpenUni, open a unidirectional stream and hand its id toConnection::bind_uni. - Send. Collect
Connection::sendablefirst (writing changes it), then for each stream offerConnection::poll_sendto the transport and report what it took withConnection::sent; keep writing a stream until the transport accepts less than offered. Bodies never go through the core:Connection::send_datahands out a DATA frame prefix, the caller writes prefix then payload (writev) and reports progress withConnection::data_written. - Receive. Feed received bytes with
Connection::recv(see its progress contract): each call consumes a prefix and the caller re-feeds the rest. - Events. Handle every
Connection::poll_event; read a header block withConnection::headersandConnection::releaseit when done.
Caller contracts:
- FIN is only ever set when the core emits
Action::FinishStream; never finish a stream on your own. - Errors. An
Errfrom any call is a state notification only.Action::CloseConnectionis the sole trigger for the wire effects of a connection error; after it (orConnection::transport_closed) keep drainingConnection::poll_eventandConnection::poll_action(Connection::peer_settingsalso keeps working); every other call returnsErr(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::FinishedorEvent::StreamAborted. A server can getStreamAbortedfor a request it never sawHeaderson (rejected at a GOAWAY cutoff, malformed, or reset by the peer before any byte arrived).Event::Closedstands in for every stream still open. - Datagrams.
Connection::parse_datagramonly routes; the caller decides whether to bufferDatagram::NotYetOpenpayloads briefly, and which requests carry datagram semantics (abort the others withH3Code::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::defaultand 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
|. - Data
Frame - DATA frame prefix (type 0x00 + length varint) to write before the payload.
- Field
Ref - A header field, borrowed: what
HeaderBlockRef::allandHeaderBlockRef::iteryield and whatConnection::send_headerstakes. - Frame
Extension - 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.
- Header
Block Id - Handle to a received header block (from
Event::Headers); stale afterConnection::releaseor connection close. - Header
Block Ref - A borrowed view of a received header block, from
Connection::headers. - Peer
Settings - 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.
- Stream
Id - A QUIC stream id (RFC 9000 section 2.1).
Enums§
- Abort
Source - 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. - Connection
Error - 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. - Headers
Kind - Which part of a message a header block is.
- Recv
- Result of
Connection::recv.consumedcounts bytes from the start of the caller’s buffer, and everyrangeindexes that buffer (within0..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. - Usage
Error - A local call the connection refused; it had no effect and no wire effect.