Skip to main content

tokio_quiche/http3/driver/
client.rs

1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use std::collections::BTreeMap;
28use std::collections::VecDeque;
29use std::sync::Arc;
30use std::time::Duration;
31
32use foundations::telemetry::log;
33use quiche::h3;
34use tokio::sync::mpsc;
35use tokio::sync::oneshot;
36
37use super::datagram;
38use super::DatagramFlowInfo;
39use super::DgramFlowId;
40use super::DriverHooks;
41use super::H3Command;
42use super::H3ConnectionError;
43use super::H3ConnectionResult;
44use super::H3Controller;
45use super::H3Driver;
46use super::H3Event;
47use super::InboundFrame;
48use super::InboundFrameStream;
49use super::InboundHeaders;
50use super::IncomingH3Headers;
51use super::OutboundFrameSender;
52use super::RequestSender;
53use super::StreamCtx;
54use super::STREAM_CAPACITY;
55use crate::http3::settings::Http3Settings;
56use crate::quic::HandshakeInfo;
57use crate::quic::QuicCommand;
58use crate::quic::QuicheConnection;
59
60/// An [H3Driver] for a client-side HTTP/3 connection. See [H3Driver] for
61/// details. Emits [`ClientH3Event`]s and expects [`ClientH3Command`]s for
62/// control.
63pub type ClientH3Driver = H3Driver<ClientHooks>;
64/// The [H3Controller] type paired with [ClientH3Driver]. See [H3Controller] for
65/// details.
66pub type ClientH3Controller = H3Controller<ClientHooks>;
67/// Receives [`ClientH3Event`]s from a [ClientH3Driver]. This is the control
68/// stream which describes what is happening on the connection, but does not
69/// transfer data.
70pub type ClientEventStream = mpsc::UnboundedReceiver<ClientH3Event>;
71/// A [RequestSender] to send HTTP requests over a [ClientH3Driver]'s
72/// connection.
73pub type ClientRequestSender = RequestSender<ClientH3Command, NewClientRequest>;
74
75/// An HTTP request sent using a [ClientRequestSender] to the [ClientH3Driver].
76#[derive(Debug)]
77pub struct NewClientRequest {
78    /// A user-defined identifier to match [`ClientH3Event::NewOutboundRequest`]
79    /// to its original [`NewClientRequest`]. This ID is not used anywhere else.
80    pub request_id: u64,
81    /// The [`h3::Header`]s that make up this request.
82    pub headers: Vec<h3::Header>,
83    /// A sender to pass the request's [`OutboundFrameSender`] to the request
84    /// body.
85    pub body_writer: Option<oneshot::Sender<OutboundFrameSender>>,
86}
87
88/// Events produced by [ClientH3Driver].
89#[derive(Debug)]
90pub enum ClientH3Event {
91    Core(H3Event),
92    /// Headers for the request with the given `request_id` were sent on
93    /// `stream_id`. The body, if there is one, could still be sending.
94    NewOutboundRequest {
95        stream_id: u64,
96        request_id: u64,
97    },
98}
99
100impl From<H3Event> for ClientH3Event {
101    fn from(ev: H3Event) -> Self {
102        Self::Core(ev)
103    }
104}
105
106/// Commands accepted by [ClientH3Driver].
107#[derive(Debug)]
108pub enum ClientH3Command {
109    Core(H3Command),
110    /// Send a new HTTP request over the [`quiche::h3::Connection`]. The driver
111    /// will allocate a stream ID and report it back to the controller via
112    /// [`ClientH3Event::NewOutboundRequest`].
113    ClientRequest(NewClientRequest),
114}
115
116impl From<H3Command> for ClientH3Command {
117    fn from(cmd: H3Command) -> Self {
118        Self::Core(cmd)
119    }
120}
121
122impl From<QuicCommand> for ClientH3Command {
123    fn from(cmd: QuicCommand) -> Self {
124        Self::Core(H3Command::QuicCmd(cmd))
125    }
126}
127
128impl From<NewClientRequest> for ClientH3Command {
129    fn from(req: NewClientRequest) -> Self {
130        Self::ClientRequest(req)
131    }
132}
133
134/// A [`PendingClientRequest`] is a request which has not yet received a
135/// response.
136///
137/// The `send` and `recv` halves are passed to the [ClientH3Controller] in an
138/// [`H3Event::IncomingHeaders`] once the server's response has been received.
139/// If this was a DATAGRAM-capable request (e.g. CONNECT-UDP), `dgram_flow`
140/// carries the pre-allocated flow channels.
141/// The lifetime of the DATAGRAM exchange is tied to the lifetime of the
142/// regular stream-based `send` and `recv`. Dropping them closes the datagram
143/// exchange.
144struct PendingClientRequest {
145    send: OutboundFrameSender,
146    recv: InboundFrameStream,
147    /// Pre-allocated DATAGRAM flow channels, if this request carries datagram
148    /// semantics. `None` for ordinary requests.
149    dgram_flow: Option<DatagramFlowInfo>,
150}
151
152/// Retry delay when `StreamBlocked` or `StreamLimit` is returned by
153/// `send_request`. Both errors are transient: the peer will open more
154/// stream credit or flow-control window within a few milliseconds.
155const BLOCKED_RETRY_DELAY: Duration = Duration::from_millis(5);
156
157pub struct ClientHooks {
158    /// Mapping from stream IDs to the associated [`PendingClientRequest`].
159    pending_requests: BTreeMap<u64, PendingClientRequest>,
160    /// Requests that could not be sent yet due to `StreamBlocked` or
161    /// `StreamLimit`. They will be retried after [`BLOCKED_RETRY_DELAY`].
162    queued_requests: VecDeque<NewClientRequest>,
163    /// A sender back into the driver's own command channel, used to
164    /// re-enqueue blocked requests after the retry delay.
165    ///
166    /// Initialised in `conn_established`; `None` before the connection
167    /// is established.
168    self_cmd_sender: Option<mpsc::UnboundedSender<ClientH3Command>>,
169}
170
171impl ClientHooks {
172    /// Returns `true` when `err` from `h3::Connection::send_request` means the
173    /// request should be retried after a short delay rather than treated as a
174    /// fatal connection error.
175    ///
176    /// `send_request` rolls back any partial stream state before returning
177    /// these errors, so retrying the call with the same arguments is safe.
178    ///
179    /// * `StreamBlocked` — the QUIC stream's flow-control window is temporarily
180    ///   exhausted; the stream entry is removed by `send_request` before
181    ///   returning, so the stream ID is not consumed.
182    /// * `TransportError(StreamLimit)` — the peer's concurrent-stream limit has
183    ///   been reached; QUIC will deliver a MAX_STREAMS frame when credit opens
184    ///   up.
185    fn is_retriable_send_error(err: &h3::Error) -> bool {
186        matches!(
187            err,
188            h3::Error::StreamBlocked |
189                h3::Error::TransportError(quiche::Error::StreamLimit)
190        )
191    }
192
193    /// Initiates a client-side request. This sends the request, stores the
194    /// [`PendingClientRequest`] and allocates a new stream plus potential
195    /// DATAGRAM flow (CONNECT-{UDP,IP}).
196    ///
197    /// If the connection temporarily cannot open a new stream (`StreamBlocked`
198    /// or `StreamLimit`), the request is pushed onto `queued_requests` and
199    /// will be retried after [`BLOCKED_RETRY_DELAY`].
200    fn initiate_request(
201        driver: &mut H3Driver<Self>, qconn: &mut QuicheConnection,
202        request: NewClientRequest,
203    ) -> H3ConnectionResult<()> {
204        let body_finished = request.body_writer.is_none();
205
206        let stream_id = match driver.conn_mut()?.send_request(
207            qconn,
208            &request.headers,
209            body_finished,
210        ) {
211            Ok(id) => id,
212            Err(ref err) if Self::is_retriable_send_error(err) => {
213                log::debug!(
214                    "send_request blocked, queuing for retry";
215                    "request_id" => request.request_id,
216                    "error" => %err,
217                );
218                driver.hooks.queued_requests.push_back(request);
219                return Ok(());
220            },
221            Err(err) => return Err(H3ConnectionError::from(err)),
222        };
223
224        // log::info!("sent h3 request"; "stream_id" => stream_id);
225        let (mut stream_ctx, send, recv) = StreamCtx::new(
226            stream_id,
227            STREAM_CAPACITY,
228            driver.ready_stream_sender.clone(),
229        );
230
231        if body_finished {
232            // `send_request()` already sent FIN for bodyless requests, so
233            // mark the send side complete and drop the outbound-frame receiver
234            // since no body will be sent.
235            H3Driver::<ClientHooks>::on_fin_sent(&mut stream_ctx)?;
236        }
237
238        let dgram_flow_info = if let Some(quarter_stream_id) =
239            datagram::extract_quarter_stream_id(stream_id, &request.headers)
240        {
241            log::info!(
242                "creating new flow for MASQUE request";
243                "stream_id" => stream_id,
244                "quarter_stream_id" => quarter_stream_id.id(),
245            );
246            stream_ctx.associated_dgram_flow_id = Some(quarter_stream_id);
247            Some(driver.establish_flow(qconn, stream_id, quarter_stream_id)?)
248        } else {
249            None
250        };
251
252        if let Some(body_writer) = request.body_writer {
253            let _ = body_writer.send(send.clone());
254            stream_ctx.wait_for_recv();
255        }
256
257        driver.insert_stream(stream_id, stream_ctx);
258        driver
259            .hooks
260            .pending_requests
261            .insert(stream_id, PendingClientRequest {
262                send,
263                recv,
264                dgram_flow: dgram_flow_info,
265            });
266
267        // Notify the H3Controller that we've allocated a stream_id for a
268        // given request_id.
269        let _ = driver
270            .h3_event_sender
271            .send(ClientH3Event::NewOutboundRequest {
272                stream_id,
273                request_id: request.request_id,
274            });
275
276        Ok(())
277    }
278
279    /// Handles a response from the peer by sending a relevant [`H3Event`] to
280    /// the [ClientH3Controller] for application-level processing.
281    fn handle_response(
282        driver: &mut H3Driver<Self>, headers: InboundHeaders,
283        pending_request: PendingClientRequest,
284    ) -> H3ConnectionResult<()> {
285        let InboundHeaders {
286            stream_id,
287            headers,
288            has_body,
289        } = headers;
290
291        let Some(stream_ctx) = driver.stream_map.get(&stream_id) else {
292            // todo(fisher): send better error to client
293            return Err(H3ConnectionError::NonexistentStream);
294        };
295
296        let headers = IncomingH3Headers {
297            stream_id,
298            headers,
299            send: pending_request.send,
300            recv: pending_request.recv,
301            read_fin: !has_body,
302            h3_audit_stats: Arc::clone(&stream_ctx.audit_stats),
303            dgram_flow: pending_request.dgram_flow,
304        };
305
306        driver
307            .h3_event_sender
308            .send(H3Event::IncomingHeaders(headers).into())
309            .map_err(|_| H3ConnectionError::ControllerWentAway)
310    }
311
312    #[cfg(test)]
313    pub(crate) fn pending_request_count(&self) -> usize {
314        self.pending_requests.len()
315    }
316}
317
318#[allow(private_interfaces)]
319impl DriverHooks for ClientHooks {
320    type Command = ClientH3Command;
321    type Event = ClientH3Event;
322
323    fn new(_settings: &Http3Settings) -> Self {
324        Self {
325            pending_requests: BTreeMap::new(),
326            queued_requests: VecDeque::new(),
327            self_cmd_sender: None,
328        }
329    }
330
331    fn conn_established(
332        driver: &mut H3Driver<Self>, qconn: &mut QuicheConnection,
333        _handshake_info: &HandshakeInfo,
334    ) -> H3ConnectionResult<()> {
335        assert!(
336            !qconn.is_server(),
337            "ClientH3Driver requires a client-side QUIC connection"
338        );
339        driver.hooks.self_cmd_sender = Some(driver.self_cmd_sender().clone());
340        Ok(())
341    }
342
343    fn headers_received(
344        driver: &mut H3Driver<Self>, _qconn: &mut QuicheConnection,
345        headers: InboundHeaders,
346    ) -> H3ConnectionResult<()> {
347        let Some(pending_request) =
348            driver.hooks.pending_requests.remove(&headers.stream_id)
349        else {
350            // todo(fisher): better handling when an unknown stream_id is
351            // encountered.
352            return Ok(());
353        };
354        Self::handle_response(driver, headers, pending_request)
355    }
356
357    fn stream_recv_closed(driver: &mut H3Driver<Self>, stream_id: u64) {
358        driver.hooks.pending_requests.remove(&stream_id);
359    }
360
361    fn stream_closed(driver: &mut H3Driver<Self>, stream_id: u64) {
362        driver.hooks.pending_requests.remove(&stream_id);
363    }
364
365    /// Client-side datagram receive policy: deliver to the flow if it exists,
366    /// otherwise drop. The client always establishes flows before
367    /// sending CONNECT requests, so receiving a datagram for an unknown flow is
368    /// either a peer misbehaviour or reordered packets after the flow has
369    /// closed.
370    fn handle_incoming_dgram(
371        driver: &mut H3Driver<Self>, flow_id: DgramFlowId, dgram: InboundFrame,
372    ) {
373        if let Some(flow) = driver.get_flow(flow_id) {
374            flow.send_best_effort(dgram);
375        }
376    }
377
378    fn conn_command(
379        driver: &mut H3Driver<Self>, qconn: &mut QuicheConnection,
380        cmd: Self::Command,
381    ) -> H3ConnectionResult<()> {
382        match cmd {
383            ClientH3Command::Core(c) => driver.handle_core_command(qconn, c),
384            ClientH3Command::ClientRequest(req) =>
385                Self::initiate_request(driver, qconn, req),
386        }
387    }
388
389    fn has_wait_action(driver: &mut H3Driver<Self>) -> bool {
390        !driver.hooks.queued_requests.is_empty()
391    }
392
393    async fn wait_for_action(
394        &mut self, qconn: &mut QuicheConnection,
395    ) -> H3ConnectionResult<()> {
396        // Sleep briefly to let the peer open stream credit or raise the
397        // MAX_STREAMS limit, then re-enqueue waiting requests back into
398        // the driver's command channel so that `conn_command` can retry them
399        // with a full `&mut H3Driver` on the next loop iteration.
400        //
401        // Only drain up to the number of available bidi streams to avoid
402        // re-queueing requests that would immediately fail.
403        tokio::time::sleep(BLOCKED_RETRY_DELAY).await;
404
405        let Some(sender) = &self.self_cmd_sender else {
406            // Should not happen: `conn_established` always sets this.
407            return Ok(());
408        };
409
410        let streams_left = qconn.peer_streams_left_bidi() as usize;
411        let to_drain = streams_left.min(self.queued_requests.len());
412
413        for request in self.queued_requests.drain(..to_drain) {
414            log::debug!(
415                "retrying queued request after stream-blocked delay";
416                "request_id" => request.request_id,
417            );
418            let _ = sender.send(ClientH3Command::ClientRequest(request));
419        }
420
421        Ok(())
422    }
423}
424
425impl ClientH3Controller {
426    /// Creates a [`NewClientRequest`] sender for the paired [ClientH3Driver].
427    pub fn request_sender(&self) -> ClientRequestSender {
428        RequestSender {
429            sender: self.cmd_sender.clone(),
430            _r: Default::default(),
431        }
432    }
433}