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
open, send, response, read.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.
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"))
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.
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.
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.
HTTP/3
Over QUIC on a datagram socket. Settings on a control stream, the QPACK dynamic table shut, and an exchange per bidirectional stream.
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.
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.
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)
| Field | Preset | What it decides |
|---|---|---|
idle | 30000 ms | how long a connection may sit before it is closed instead |
max_idle | 8 | idle connections kept per key |
max_total | 64 | connections held open at once |
liveness | Peek | what is asked of a connection before reuse |
wait | 5000 ms | how long a caller waits for a free slot |
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.
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.
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.