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}