Table of Contents
Streams, cancellation, and 0-RTT
Closing versus cancelling a stream, and the two ways to send data before a handshake finishes.
A Conn carries many streams. Getting their lifecycle right is most of what
separates a protocol that works from one that hangs or wastes bandwidth. This
page covers the parts that are easy to get wrong.
Upstream covers the same ground for Rust in Using QUIC.
Closing is not cancelling
Two different operations, routinely confused:
| Call | Meaning |
|---|---|
Stream.Close() |
finish sending — the peer reads EOF after buffered data |
Stream.CancelWrite(code) |
abandon sending — discard buffered data, tell the peer |
Stream.CancelRead(code) |
stop receiving — tell the peer to stop sending |
Close is graceful and belongs at the end of a successful exchange. It closes
only the send side of a bidirectional stream, which is what lets the peer’s
io.Copy or io.ReadAll return. Forgetting it is the most common way to hang
a request/response protocol — see Build an echo protocol.
CancelWrite and CancelRead are the abort path. They exist because QUIC can
stop a transfer that is no longer wanted:
s, err := conn.OpenStreamSync(ctx)
// ... decide the response is stale or the user navigated away
s.CancelRead(1) // stop the peer sending more
s.CancelWrite(1) // discard anything we have queued
The reason to reach for these rather than just closing the connection: on a multiplexed connection, a stream you no longer care about still consumes bandwidth and can delay the streams you do care about. Cancelling reclaims that capacity without disturbing the other streams. For anything real-time — media, interactive UI, speculative prefetch — this is the difference between a responsive protocol and one that falls behind under load.
Both take an application error code, an opaque uint64 your protocol defines.
The unidirectional halves have the matching subset: SendStream.CancelWrite
and ReceiveStream.CancelRead.
0-RTT: sending before the handshake completes
Normally Endpoint.Connect blocks until the peer is authenticated. Two APIs
let you send earlier, and they are not symmetric.
Client side — ConnectEarly and Into0RTT
c, err := ep.ConnectEarly(ctx, addr, alpn) // returns immediately
if err != nil {
return err
}
conn, ok := c.Into0RTT() // ok reports whether 0-RTT is actually available
if !ok {
conn, err = c.Connection(ctx) // fall back to the verified path
}
Into0RTT returns ok = false when there is no usable session ticket for
this peer — the first connection to a peer can never be 0-RTT. Always write
the fallback.
Those tickets come from iroh.SessionCache, an LRU of TLS 1.3 session tickets
bucketed by TLS server name. Because iroh derives a unique server name from
each peer’s endpoint ID, tickets for different peers cannot collide and a
resumption always targets the identity you meant. You get this by default;
there is nothing to wire up.
A Connecting is not safe for concurrent use and may be consumed only
once: after Into0RTT succeeds or Connection returns, do not touch it
again.
Server side — Accepting.Into0RTT
The accept side has the mirror image: an Accepting whose handshake may still
be in flight, with Into0RTT() to get a connection that can read early data
and Connection(ctx) to wait for the verified one.
The safety rule
Early data arrives before the peer is authenticated and can be replayed by an attacker who captured it. So handle it only if both hold:
- the operation is idempotent — replaying it changes nothing; and
- it does not depend on the peer’s identity, because
RemoteIDis not yet proven.
A read is usually fine. A write, a payment, or anything authorizing on identity is not. When you need the guarantee, wait:
<-conn.HandshakeComplete() // now RemoteID is proven
After the fact, conn.Used0RTT() reports whether the connection actually used
early data, which is what you check before trusting anything received on it.
Next steps
- Endpoints and connections — the rest of the connection API.
- Observing and rejecting connections — admission control, including the pre-handshake filter.
- Security and privacy — the wider threat model.