The connection layer

One request.
Four protocols.
One contract.

HTTP/1.1, HTTP/2, HTTP/3 and WebSocket all answer the same eight methods — open an exchange, send a body, read a head and a body. What carries it is a detail the code above never sees.

HTTP/1.1HTTP/2HTTP/3WebSocket

one call · tls 1.3 · :443four carriers
CLIENT SERVER conn.open() request response head · body http/1.1 http/2 http/3 ws
Four wires, one set of methods: open, send, response, read.
01

A request, and what came back

The whole of it: open an exchange, read the head, read the body to its end. Nothing in this code says HTTP/2 — and nothing would change if it were HTTP/1.1 or HTTP/3.

get.mbt
async fn get(conn : &@spec.Conn, path : String) -> String {
  let stream = conn.open(@spec.Request::new("GET", path))
  let response = conn.response(stream)
  let body = @spec.read_to_end(conn, stream)
  "\{response.status} \{body.length()}"
}

// Two adapters, one function.
let h2 = @h2.connect("example.com", port=443, tls=tls)
let h1 = @h1.connect("example.com", port=443, tls=tls)
println(get(h2, "/status"))
println(get(h1, "/status"))
02

Four implementations, one shape

Each adapter is a client you can name directly, or a Conn you can hand to code written against the contract.

RFC 9112

HTTP/1.1

Bodies by length, by chunk, or to the close. A reply that stops short of its length raises instead of returning half a body. One exchange at a time, and it says so.

one exchange · content-length
chunked · close
RFC 9113

HTTP/2

Preface and settings on connect, an exchange per stream, flow control both ways. A body larger than the window still goes out; a reply larger than it still arrives.

preface · settings
a stream per exchange
RFC 9114

HTTP/3

Over QUIC on a datagram socket. Settings on a control stream, the QPACK dynamic table shut, and an exchange per bidirectional stream.

quic datagrams
qpack static
RFC 6455

WebSocket

The upgrade is the exchange; after it, a read yields whole messages, a ping is answered before you see the next one, and a close keeps its code.

101 upgrade
messages · ping · close
03

Connections kept, not reopened

A pool keyed by what makes two connections interchangeable: the host, the port, and the protocol ALPN settled on. A connection opened for h2 is never handed out as a clear HTTP/1.1 one.

pool.mbt
let pool = @pool.Pool::new(
  key => @tcp.dial(key.host, port=key.port, tls=tls_for(key)),
  config={ ..@pool.defaults, idle: 60000 },
)
let key = @pool.Key::new("example.com", 443, alpn="h2")
let conn = pool.get(key)
// ... one connection's worth of work ...
pool.put(key, conn)
FieldPresetWhat it decides
idle30000 mshow long a connection may sit before it is closed instead
max_idle8idle connections kept per key
max_total64connections held open at once
livenessPeekwhat is asked of a connection before reuse
wait5000 mshow long a caller waits for a free slot
04

One line to a connection

The scheme decides TLS, the ALPN list is offered, and the chain is judged against Mozilla's roots unless you bring your own. What comes back says which protocol the server chose.

connect.mbt
let opened = @moonnet.connect("https://example.com/status", alpn=["h2", "http/1.1"])
println(opened.alpn)   // h2
println(opened.target) // /status

let conn = @h2.client(opened.conn, scheme="https", authority="example.com")
let (response, body) = @spec.call(conn, @spec.Request::new("GET", opened.target))

Below the adapters are the two transports themselves: a TCP connection that is plain or TLS 1.3 and implements the async reader and writer, a listener that serves both on one port, and a datagram socket carrying a QUIC connection.

05

Read on

The guide walks from an echo server to a pooled HTTP/3 client. The reference lists every public item. The showcase shows the four protocols carrying the same request, recorded from real runs.