pub async fn handle(
quic: Connection,
config: Arc<Config>,
shutdown: Shutdown,
resolver: &ResolverBudget,
tunnels: Arc<AtomicU64>,
dropped_datagrams: Arc<AtomicU64>,
authenticated: AuthGate,
) -> Result<(), ConnectionError>Expand description
Drives one QUIC connection until the peer stops sending requests.
Returns Err only for connection-level failures; per-request problems are
logged and confined to their own stream.
tunnels counts the requests that get a tunnel slot here; it belongs to
crate::quic, which reads it after this returns (D72).
§Shutdown
When shutdown fires this sends a GOAWAY and then keeps the connection alive
until its tunnels finish, so an in-flight page load or call is not cut off
mid-sentence. Two details make that work:
- dropping the HTTP/3 connection closes the QUIC connection, so returning early would kill the very tunnels being drained — the loop has to stay;
accept()cannot report the end of the drain: a client GOAWAY says nothing about the requests already in flight, socrate::h3api::Connection::acceptnever reports one. The connection going idle is the signal instead —crate::tunnel::Quota::is_busy, which counts accepted requests as well as open tunnels. Both halves are needed: a request stream is accepted the moment the peer opens it and does not take a tunnel slot until its headers have arrived and its credentials have been checked, so a drain that watched only the tunnel count reported “nothing left to do” while a request below the GOAWAY identifier was still being read. RFC 9114 §5.2 tells the peer such requests “might have been processed” and leaves a server the choice of rejecting them individually (REQUEST_REJECTED, §4.1.1, so the client knows to retry); this server’s choice is to serve them, and closing the connection mid-request is neither.
The wait for the tunnels is deliberately unbounded here: the grace period
belongs to the endpoint (crate::quic), which closes everything when it
expires. Bounding it in both places would mean two timeouts to keep
consistent.
Sending the GOAWAY is a different matter and is bounded, because it is a
write and the peer decides when a write completes: the control stream
carries the peer’s flow control, and this one sits in the same select! as
the drain, so a peer granting no window used to park the drain behind it for
the whole grace period. A frame that cannot be sent within one idle timeout
is given up on and the connection drains without it.
§The unauthenticated bound
A connection has SILENCE_FACTOR idle timeouts from the handshake to get
one request past the credentials check, after which it is closed with
H3_NO_ERROR. Without a bound of some kind a peer that completes the QUIC
handshake and then says nothing holds a max_connections slot for as long
as it keeps its socket open, since the keep-alive PINGs quic.rs sends are
answered by its QUIC stack with no application involved and so keep the
transport’s idle timeout from ever firing (D76). Once a request
authenticates the bound is gone for the life of the connection.
The deadline is absolute rather than rearmed on each wait, which is the difference between bounding a connection and bounding a pause in it: a request stream is accepted the moment the peer opens it, so a peer that opened one every other idle timeout — a byte apiece, never a request, never a credential — used to reset the timer for ever and hold the slot anyway (review C1’). Nothing legitimate is near it: a client that has just completed two handshakes sends its first request within a round trip.
The deadline bounds how long such a connection may last; what bounds how
much of this server it may occupy while it does is a second, transport-level
clamp on its request streams (quic::INITIAL_BIDI_STREAMS), lifted by the
same request that lifts this one.
resolver is the server’s blocking-pool allowance; the connection takes its
own view of it here, which is what bounds the threads its name lookups can
hold (D90).
authenticated is that state, and it belongs to crate::quic for the same
reason tunnels does: the accept loop reads it after this has been handed
the connection, to decide which connection loses its slot when the server is
full. dropped_datagrams belongs there too — it is read for the closing
line, at the same moment tunnels is — and is handed on to the HTTP/3
connection, whose datagram router is the thing that drops.