Skip to main content

handle

Function handle 

Source
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, so crate::h3api::Connection::accept never 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.