trillium_http/h3/connection.rs
1mod peer_settings_wait;
2
3use super::{
4 H3Error,
5 frame::{Frame, FrameDecodeError, UniStreamType},
6 quic_varint::{self, QuicVarIntError},
7 settings::H3Settings,
8};
9use crate::{
10 Buffer, Conn, HttpContext, KnownHeaderName, PeerGone, Priority,
11 conn::H3FirstFrame,
12 h3::{H3ErrorCode, MAX_BUFFER_SIZE},
13 headers::qpack::{DecoderDynamicTable, EncoderDynamicTable, FieldSection},
14 http_context::ConnectionContext,
15};
16use event_listener::Event;
17use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
18use std::{
19 future::{Future, IntoFuture},
20 io::{self, ErrorKind},
21 pin::Pin,
22 sync::{
23 Arc, OnceLock,
24 atomic::{AtomicBool, AtomicU64, Ordering},
25 },
26 task::{Context, Poll},
27};
28use swansong::{ShutdownCompletion, Swansong};
29
30/// The result of processing an HTTP/3 bidirectional stream.
31#[derive(Debug)]
32pub enum H3StreamResult<Transport> {
33 /// The stream carried a normal HTTP/3 request.
34 Request(Conn<Transport>),
35
36 /// The stream carries a WebTransport bidirectional data stream. The `session_id` identifies
37 /// the associated WebTransport session.
38 WebTransport {
39 /// The WebTransport session ID (stream ID of the CONNECT request).
40 session_id: u64,
41 /// The underlying transport, ready for application data.
42 transport: Transport,
43 /// Any bytes buffered after the session ID during stream negotiation.
44 buffer: Buffer,
45 },
46}
47
48/// Inner-loop result of [`H3Connection::process_inbound_uni_with_close`] before the recv
49/// stream is reattached. Decouples the inner async block (which only borrows the stream)
50/// from the caller-visible [`UniStreamResult`] (which returns the stream by value on
51/// non-`Handled` variants), so the function can keep ownership of `stream` long enough to
52/// fire its close callback before `stream` drops.
53enum UniInnerResult {
54 Handled,
55 WebTransport { session_id: u64, buffer: Buffer },
56 Unknown { stream_type: u64 },
57}
58
59/// The result of processing an HTTP/3 unidirectional stream.
60#[derive(Debug)]
61pub enum UniStreamResult<T> {
62 /// The stream was a known internal type (control, QPACK encoder/decoder) and was handled
63 /// automatically.
64 Handled,
65
66 /// A WebTransport unidirectional data stream. The `session_id` identifies the associated
67 /// WebTransport session.
68 WebTransport {
69 /// The WebTransport session ID.
70 session_id: u64,
71 /// The receive stream, ready for application data.
72 stream: T,
73 /// Any bytes buffered after the session ID during stream negotiation.
74 buffer: Buffer,
75 },
76
77 /// A stream whose type is recognized but unsupported (e.g. `Push`) or not recognized
78 /// at all by this crate.
79 ///
80 /// The caller is responsible for disposing of the stream — the in-tree consumers RST
81 /// it with `H3_STREAM_CREATION_ERROR`. `process_inbound_uni` deliberately does *not*
82 /// close the stream itself: handing it back gives a downstream extension the option to
83 /// implement a stream type trillium-http doesn't know about (a future RFC, an
84 /// experiment, etc.) without forking the codec.
85 Unknown {
86 /// The raw stream type value.
87 stream_type: u64,
88 /// The stream.
89 stream: T,
90 },
91}
92
93/// Shared state for a single HTTP/3 QUIC connection.
94///
95/// Call the appropriate methods on this type for each stream accepted from the QUIC connection.
96///
97/// # Driver shape (vs h2)
98///
99/// h2 multiplexes everything onto a single TCP byte stream, so a single
100/// [`H2Driver`][crate::h2::H2Driver] task suffices. h3 instead has the QUIC layer hand us multiple
101/// independent streams: an inbound and outbound control stream, an inbound and outbound QPACK
102/// encoder stream, an inbound and outbound QPACK decoder stream, and one bidi stream per
103/// request. There is no single "h3 driver" — each stream is driven by its own future returned from
104/// `H3Connection`'s `run_*` / `process_*` methods, and the caller decides how those futures are
105/// scheduled.
106///
107/// The trillium-http boundary is **runtime-free by design**: this crate hands out anonymous futures
108/// and lets the caller pick the executor. The in-tree consumers (`trillium-server-common`,
109/// `trillium-client`) follow a task-per-stream pattern — spawn each long-lived control / encoder /
110/// decoder future on its own task at connection setup, then spawn one task per accepted request
111/// stream. Nothing in this crate requires that pattern; a caller could in principle race all the
112/// futures on one task instead, with different perf characteristics.
113#[derive(Debug)]
114pub struct H3Connection {
115 /// Shared configuration across all protocols.
116 context: ConnectionContext,
117
118 /// Connection-scoped shutdown signal. Shut down when we receive GOAWAY from the peer or when
119 /// the server-level Swansong shuts down. Request stream tasks use this to interrupt
120 /// in-progress work.
121 swansong: Swansong,
122
123 /// The peer's H3 settings, received on their control stream. Request streams may need to
124 /// consult these (e.g. max field section size).
125 pub(super) peer_settings: OnceLock<H3Settings>,
126
127 /// Multi-listener wake source for
128 /// [`PeerSettingsReady`][peer_settings_wait::PeerSettingsReady]. Notified by
129 /// `run_inbound_control` after applying peer SETTINGS, and again on connection
130 /// close, so any number of concurrently-parked futures all unblock together.
131 pub(super) peer_settings_event: Event,
132
133 /// The highest bidirectional stream ID we have accepted. Used to compute the GOAWAY value
134 /// (this + 4) to tell the peer which requests we saw. None until the first stream is accepted.
135 /// Updated by the runtime adapter's accept loop via [`record_accepted_stream`].
136 max_accepted_stream_id: AtomicU64,
137
138 /// Whether we have accepted any streams yet.
139 has_accepted_stream: AtomicBool,
140
141 /// The decoder-side QPACK dynamic table for this connection.
142 decoder_dynamic_table: DecoderDynamicTable,
143
144 /// The encoder-side QPACK dynamic table for this connection.
145 encoder_dynamic_table: EncoderDynamicTable,
146
147 /// Sink for RFC 9218 priority signals, set via
148 /// [`register_priority_callback`][Self::register_priority_callback]. Unset until the runtime
149 /// adapter that owns the QUIC streams registers it.
150 priority_callback: PriorityCallback,
151}
152
153/// Boxed sink for `(stream_id, priority, is_update)` signals.
154type PriorityCallbackFn = Box<dyn Fn(u64, Priority, bool) + Send + Sync>;
155
156/// A registered sink for `(stream_id, priority, is_update)` signals. Newtype so [`H3Connection`]
157/// can keep deriving `Debug` despite holding a boxed closure.
158#[derive(Default)]
159struct PriorityCallback(OnceLock<PriorityCallbackFn>);
160
161impl std::fmt::Debug for PriorityCallback {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_tuple("PriorityCallback")
164 .field(&self.0.get().map(|_| format_args!("..")))
165 .finish()
166 }
167}
168
169impl H3Connection {
170 /// Construct a new `H3Connection` to manage HTTP/3 for a given peer.
171 pub fn new(context: Arc<HttpContext>) -> Arc<Self> {
172 let swansong = context.swansong.child();
173 let max_table_capacity = context.config.dynamic_table_capacity;
174 let blocked_streams = context.config.h3_blocked_streams;
175 let encoder_dynamic_table = EncoderDynamicTable::new(&context);
176 Arc::new(Self {
177 context: Arc::new(context),
178 swansong,
179 peer_settings: OnceLock::new(),
180 peer_settings_event: Event::new(),
181 max_accepted_stream_id: AtomicU64::new(0),
182 has_accepted_stream: AtomicBool::new(false),
183 decoder_dynamic_table: DecoderDynamicTable::new(max_table_capacity, blocked_streams),
184 encoder_dynamic_table,
185 priority_callback: PriorityCallback::default(),
186 })
187 }
188
189 /// Register the sink for RFC 9218 priority signals on this connection.
190 ///
191 /// The callback is invoked with `(stream_id, priority, is_update)` once per request when its
192 /// initial `priority` header is parsed (`is_update = false`), and again for every
193 /// `PRIORITY_UPDATE` received afterward (`is_update = true`). `is_update` lets the receiver
194 /// honor RFC 9218 precedence: a `PRIORITY_UPDATE` outranks the request's initial header
195 /// priority regardless of arrival order, including when it arrives before the stream is
196 /// accepted.
197 ///
198 /// This crate keeps no priority state of its own and does no scheduling: it parses each
199 /// signal and hands the [`Priority`] off through this callback, leaving the receiver to apply
200 /// it to whatever owns send scheduling. Without a registered callback, priority is parsed but
201 /// never applied.
202 ///
203 /// Has no effect if a callback is already registered.
204 pub fn register_priority_callback(
205 &self,
206 callback: impl Fn(u64, Priority, bool) + Send + Sync + 'static,
207 ) {
208 let _ = self.priority_callback.0.set(Box::new(callback));
209 }
210
211 /// Emit a priority signal for a request stream to the registered callback, if any.
212 /// `is_update` distinguishes a received `PRIORITY_UPDATE` from the request's initial header
213 /// priority so the receiver can honor the precedence rule.
214 fn emit_priority(&self, stream_id: u64, priority: Priority, is_update: bool) {
215 let kind = if is_update {
216 "PRIORITY_UPDATE"
217 } else {
218 "initial"
219 };
220 match self.priority_callback.0.get() {
221 Some(callback) => {
222 log::trace!("H3 stream {stream_id}: emitting {kind} priority \"{priority}\"");
223 callback(stream_id, priority, is_update);
224 }
225 None => log::trace!(
226 "H3 stream {stream_id}: {kind} priority \"{priority}\" parsed but no callback \
227 registered"
228 ),
229 }
230 }
231
232 /// Handle an RFC 9218 `PRIORITY_UPDATE` received on the peer's control stream. The
233 /// prioritized element id must name a client-initiated bidirectional (request) stream —
234 /// `id % 4 == 0` in QUIC — and other ids are ignored rather than erroring, since the signal
235 /// is advisory.
236 fn emit_priority_update(&self, prioritized_element_id: u64, priority: Priority) {
237 if prioritized_element_id.is_multiple_of(4) {
238 self.emit_priority(prioritized_element_id, priority, true);
239 } else {
240 log::trace!(
241 "H3: ignoring PRIORITY_UPDATE for non-request stream {prioritized_element_id}"
242 );
243 }
244 }
245
246 /// Retrieve the [`Swansong`] shutdown handle for this HTTP/3 connection. See also
247 /// [`H3Connection::shut_down`]
248 pub fn swansong(&self) -> &Swansong {
249 &self.swansong
250 }
251
252 /// Attempt graceful shutdown of this HTTP/3 connection (all streams).
253 ///
254 /// The returned [`ShutdownCompletion`] type can
255 /// either be awaited in an async context or blocked on with [`ShutdownCompletion::block`] in a
256 /// blocking context
257 ///
258 /// Note that this will NOT shut down the server. To shut down the whole server, use
259 /// [`HttpContext::shut_down`]
260 pub fn shut_down(&self) -> ShutdownCompletion {
261 // Wake any in-flight `decode_field_section` calls parked on the decoder
262 // table's `ThresholdWait` (a non-I/O future awaiting dynamic-table inserts
263 // from the peer). The encoder table's writer loop is already swansong-
264 // aware, but we mark it failed too for symmetry: any future state
265 // mutations after shutdown are no longer wire-relevant.
266 self.decoder_dynamic_table.fail(H3ErrorCode::NoError);
267 self.encoder_dynamic_table.fail(H3ErrorCode::NoError);
268 self.wake_peer_settings_waiters();
269 self.swansong.shut_down()
270 }
271
272 /// Retrieve the [`HttpContext`] for this server.
273 pub fn context(&self) -> Arc<HttpContext> {
274 Arc::clone(&*self.context)
275 }
276
277 /// This connection's handle on its [`HttpContext`], for each of its streams' conns to share.
278 pub(crate) fn connection_context(&self) -> ConnectionContext {
279 Arc::clone(&self.context)
280 }
281
282 /// Returns the peer's HTTP/3 settings, available once the peer's control stream has been
283 /// processed.
284 pub fn peer_settings(&self) -> Option<&H3Settings> {
285 self.peer_settings.get()
286 }
287
288 /// Record that we accepted a bidirectional stream with this ID.
289 fn record_accepted_stream(&self, stream_id: u64) {
290 self.max_accepted_stream_id
291 .fetch_max(stream_id, Ordering::Relaxed);
292 self.has_accepted_stream.store(true, Ordering::Relaxed);
293 }
294
295 /// The stream ID to send in a GOAWAY frame: one past the highest stream we accepted, or 0 if we
296 /// haven't accepted any.
297 fn goaway_id(&self) -> u64 {
298 if self.has_accepted_stream.load(Ordering::Relaxed) {
299 self.max_accepted_stream_id.load(Ordering::Relaxed) + 4
300 } else {
301 0
302 }
303 }
304
305 /// Begin processing a single HTTP/3 request-response cycle on an accepted bidirectional
306 /// stream.
307 ///
308 /// Returns a builder. Attach an optional reset hook with
309 /// [`with_reset`][H3BidiRequest::with_reset], then `.await` it to run one request/response
310 /// cycle. Awaiting resolves to [`H3StreamResult::WebTransport`] if the stream opens a
311 /// WebTransport session rather than a standard HTTP/3 request.
312 ///
313 /// Without a reset hook, a stream-level protocol error drops the transport without
314 /// resetting it; attach `with_reset` to issue the RST that RFC 9114 requires for stream
315 /// errors.
316 ///
317 /// RFC 9218 priority is delivered out of band via the callback registered with
318 /// [`register_priority_callback`][Self::register_priority_callback]: this method emits the
319 /// request's initial priority once the headers are parsed.
320 pub fn process_inbound_bidi<Transport, Handler>(
321 self: Arc<Self>,
322 transport: Transport,
323 handler: Handler,
324 stream_id: u64,
325 ) -> H3BidiRequest<Transport, Handler> {
326 H3BidiRequest {
327 h3: self,
328 transport,
329 handler,
330 stream_id,
331 reset: None,
332 reject_requests: false,
333 peer_gone: None,
334 }
335 }
336
337 /// Process a single HTTP/3 request-response cycle on a bidirectional stream, calling
338 /// `reset` to issue a stream RST when a stream-level protocol error occurs.
339 ///
340 /// On any `H3Error::Protocol(code)` produced by first-frame processing (HEADERS decode,
341 /// pseudo-header validation, etc.), `reset` is invoked with the still-owned transport and
342 /// the error code before the error is returned. This lets callers RST both the recv and
343 /// send halves of the bidi stream — required by RFC 9114 for stream errors like
344 /// `H3_MESSAGE_ERROR`. I/O errors and successful runs do not invoke `reset`.
345 ///
346 /// `reset` is a `FnOnce` taking `(&mut Transport, H3ErrorCode)`. trillium-http does not
347 /// itself depend on any reset capability of the transport; callers wire up the actual
348 /// stream-RST mechanism (e.g. quinn's `RecvStream::stop` + `SendStream::reset`) inside
349 /// the closure.
350 ///
351 /// # Errors
352 ///
353 /// Returns an `H3Error` in case of io error or http/3 semantic error.
354 // This is not deprecated yet because it didn't make sense to release a new version of
355 // trillium-client just to avoid this deprecation, but the intention is to deprecate
356 pub async fn process_inbound_bidi_with_reset<Transport, Handler, Fut, Reset>(
357 self: Arc<Self>,
358 mut transport: Transport,
359 handler: Handler,
360 stream_id: u64,
361 reset: Reset,
362 ) -> Result<H3StreamResult<Transport>, H3Error>
363 where
364 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
365 Handler: FnOnce(Conn<Transport>) -> Fut,
366 Fut: Future<Output = Conn<Transport>>,
367 Reset: FnOnce(&mut Transport, H3ErrorCode),
368 {
369 self.record_accepted_stream(stream_id);
370 let _guard = self.swansong.guard();
371 let mut buffer: Buffer =
372 Vec::with_capacity(self.context.config.request_buffer_initial_len).into();
373
374 let outcome =
375 Conn::process_first_frame_h3(&self, &mut transport, &mut buffer, stream_id).await;
376
377 match outcome {
378 Ok(H3FirstFrame::Request {
379 validated,
380 start_time,
381 }) => {
382 let conn = Conn::build_h3(
383 self, transport, buffer, validated, start_time, stream_id, None,
384 );
385 Ok(H3StreamResult::Request(
386 handler(conn).await.send_h3().await?,
387 ))
388 }
389 Ok(H3FirstFrame::WebTransport { session_id }) => Ok(H3StreamResult::WebTransport {
390 session_id,
391 transport,
392 buffer,
393 }),
394 Err(error) => {
395 if let H3Error::Protocol(code) = &error {
396 reset(&mut transport, *code);
397 }
398 Err(error)
399 }
400 }
401 }
402
403 /// Decode a QPACK-encoded field section, consulting the dynamic table as needed.
404 ///
405 /// If the field section's Required Insert Count is greater than zero, waits until the
406 /// dynamic table has received enough entries. Returns an error on protocol violations or
407 /// if the encoder stream fails while waiting.
408 ///
409 /// Duplicate pseudo-headers are silently ignored (first value wins). Unknown
410 /// pseudo-headers are rejected.
411 ///
412 /// # Errors
413 ///
414 /// Returns an error if the encoded bytes cannot be parsed as a valid field section.
415 #[cfg(feature = "unstable")]
416 pub async fn decode_field_section(
417 &self,
418 encoded: &[u8],
419 stream_id: u64,
420 ) -> Result<FieldSection<'static>, H3Error> {
421 self.decoder_dynamic_table.decode(encoded, stream_id).await
422 }
423
424 #[cfg(not(feature = "unstable"))]
425 pub(crate) async fn decode_field_section(
426 &self,
427 encoded: &[u8],
428 stream_id: u64,
429 ) -> Result<FieldSection<'static>, H3Error> {
430 self.decoder_dynamic_table.decode(encoded, stream_id).await
431 }
432
433 /// Encode a QPACK field section (no HTTP/3 framing) from pseudo-headers and headers,
434 /// consulting the encoder dynamic table to emit literal-with-name-reference or indexed
435 /// representations as the table's contents allow.
436 ///
437 /// Superseded by [`encode_field_section_framed`][Self::encode_field_section_framed], which
438 /// also frames the section and enforces the peer's field-section size limit.
439 ///
440 /// # Errors
441 ///
442 /// Returns an `H3Error` in case of http/3 semantic error.
443 // Retained only so an older `trillium-client` that predates the framed method still builds
444 // against this crate; remove it at the next breaking release.
445 #[cfg(feature = "unstable")]
446 #[allow(clippy::unnecessary_wraps, reason = "future-proofing api")]
447 pub fn encode_field_section(
448 &self,
449 field_section: &FieldSection<'_>,
450 buf: &mut Vec<u8>,
451 stream_id: u64,
452 ) -> Result<(), H3Error> {
453 self.encoder_dynamic_table
454 .encode(field_section, buf, stream_id);
455 Ok(())
456 }
457
458 /// Encode `field_section` as a complete HTTP/3 HEADERS frame — the QPACK-compressed field
459 /// section prefixed with its `type` + `length` frame header — and append it to `buffer`.
460 ///
461 /// If the peer's `SETTINGS_MAX_FIELD_SECTION_SIZE` is known and the section's
462 /// [`uncompressed_len`][crate::headers::FieldSection] exceeds it, the section is rejected
463 /// before encoding and nothing is written. Until the peer's SETTINGS arrive the limit is
464 /// unknown and unenforced (RFC 9114 §4.2.2).
465 ///
466 /// # Errors
467 ///
468 /// [`io::ErrorKind::InvalidData`] if the field section exceeds the peer's advertised limit, or
469 /// the QPACK encoder's error mapped through [`io::Error::other`].
470 #[cfg(feature = "unstable")]
471 #[doc(hidden)]
472 pub fn encode_field_section_framed(
473 &self,
474 field_section: &FieldSection<'_>,
475 buffer: &mut Vec<u8>,
476 stream_id: u64,
477 ) -> io::Result<()> {
478 self.encode_field_section_framed_impl(field_section, buffer, stream_id)
479 }
480
481 #[cfg(not(feature = "unstable"))]
482 pub(crate) fn encode_field_section_framed(
483 &self,
484 field_section: &FieldSection<'_>,
485 buffer: &mut Vec<u8>,
486 stream_id: u64,
487 ) -> io::Result<()> {
488 self.encode_field_section_framed_impl(field_section, buffer, stream_id)
489 }
490
491 fn encode_field_section_framed_impl(
492 &self,
493 field_section: &FieldSection<'_>,
494 buffer: &mut Vec<u8>,
495 stream_id: u64,
496 ) -> io::Result<()> {
497 // Enforce the peer's SETTINGS_MAX_FIELD_SECTION_SIZE against the uncompressed size the
498 // limit is defined in (RFC 9114 §4.2.2), before encoding — an over-limit section is
499 // rejected without being written to the wire.
500 if let Some(max_size) = self
501 .peer_settings()
502 .and_then(H3Settings::max_field_section_size)
503 {
504 let size = field_section.uncompressed_len();
505 if size > max_size {
506 return Err(io::Error::new(
507 ErrorKind::InvalidData,
508 format!("field section would be longer than peer allows ({size} > {max_size})"),
509 ));
510 }
511 }
512
513 let start = buffer.len();
514 self.encoder_dynamic_table
515 .encode(field_section, buffer, stream_id);
516
517 // The HEADERS frame length is the encoded (compressed) payload length, distinct from the
518 // uncompressed size enforced above. Encode the section in place, then open a gap in front
519 // of it for the now-known frame header and shift the section right into place.
520 let section_len = buffer.len() - start;
521 let frame = Frame::Headers(section_len as u64);
522 let frame_header_len = frame.encoded_len();
523 buffer.resize(buffer.len() + frame_header_len, 0);
524 buffer.copy_within(start..start + section_len, start + frame_header_len);
525 frame.encode(&mut buffer[start..start + frame_header_len]);
526 Ok(())
527 }
528
529 /// Run this connection's HTTP/3 outbound control stream.
530 ///
531 /// Sends the initial SETTINGS frame, then sends GOAWAY when the connection shuts down.
532 /// Returns after GOAWAY is sent; keep the stream open until the QUIC connection closes
533 /// (closing a control stream is a connection error).
534 ///
535 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return, for the same reason
536 /// as [`run_encoder`][Self::run_encoder].
537 ///
538 /// # Errors
539 ///
540 /// Returns an `H3Error` in case of io error or http/3 semantic error.
541 pub async fn run_outbound_control<T>(&self, mut stream: T) -> Result<(), H3Error>
542 where
543 T: AsyncWrite + Unpin + Send,
544 {
545 let result: Result<(), H3Error> = async {
546 let mut buf = vec![0; 128];
547
548 let settings = Frame::Settings(H3Settings::from(&self.context.config));
549 log::trace!(
550 "H3 outbound control: sending SETTINGS: {:?}",
551 H3Settings::from(&self.context.config)
552 );
553
554 write(&mut buf, &mut stream, |buf| {
555 let mut written = quic_varint::encode(UniStreamType::Control, buf)?;
556 written += settings.encode(&mut buf[written..])?;
557 Some(written)
558 })
559 .await?;
560 log::trace!("H3 outbound control: SETTINGS sent");
561
562 self.swansong.clone().await;
563
564 write(&mut buf, &mut stream, |buf| {
565 Frame::Goaway(self.goaway_id()).encode(buf)
566 })
567 .await?;
568
569 Ok(())
570 }
571 .await;
572
573 self.shut_down();
574 result
575 }
576
577 /// Run the outbound QPACK encoder stream for the duration of the connection.
578 ///
579 /// Writes the stream type byte, then drains encoder-stream instructions from the encoder
580 /// dynamic table as they are enqueued. Returns when the connection shuts down or the table is
581 /// marked failed.
582 ///
583 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return. This stream is
584 /// mandatory for the connection's lifetime, so its termination — clean or errored — means the
585 /// connection can no longer function; marking it shut down lets a pooling caller evict it
586 /// rather than hand it back out. Idempotent on the clean path, which returns only after
587 /// shutdown has already begun.
588 ///
589 /// # Errors
590 ///
591 /// Returns an `H3Error` in case of io error.
592 pub async fn run_encoder<T>(&self, mut stream: T) -> Result<(), H3Error>
593 where
594 T: AsyncWrite + Unpin + Send,
595 {
596 let result = self
597 .encoder_dynamic_table
598 .run_writer(&mut stream, self.swansong.clone())
599 .await;
600 self.shut_down();
601 result
602 }
603
604 /// Run the outbound QPACK decoder stream for the duration of the connection.
605 ///
606 /// Writes the stream type byte, then loops sending Section Acknowledgement and Insert
607 /// Count Increment instructions as they become needed. Returns when the connection
608 /// shuts down.
609 ///
610 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return, for the same reason
611 /// as [`run_encoder`][Self::run_encoder].
612 ///
613 /// # Errors
614 ///
615 /// Returns an `H3Error` in case of io error or http/3 semantic error.
616 pub async fn run_decoder<T>(&self, mut stream: T) -> Result<(), H3Error>
617 where
618 T: AsyncWrite + Unpin + Send,
619 {
620 let result = self
621 .decoder_dynamic_table
622 .run_writer(&mut stream, self.swansong.clone())
623 .await;
624 self.shut_down();
625 result
626 }
627
628 /// Handle an inbound unidirectional HTTP/3 stream from the peer.
629 ///
630 /// Internal stream types (control, QPACK encoder/decoder) are handled automatically;
631 /// application streams are returned via [`UniStreamResult`] for the caller to process.
632 ///
633 /// On a connection-level protocol error, this method drops the recv stream before
634 /// the caller can react. Quinn's `RecvStream::drop` then sends `STOP_SENDING`, which
635 /// races against the caller's `connection.close` — if the peer responds with a
636 /// malformed `RESET_STREAM` (notably `final_offset = 0`) before our app close is
637 /// applied, the transport-level error overrides our app error code on the wire.
638 /// Use [`process_inbound_uni_with_close`] to thread the close call through the
639 /// function so it fires before the stream drops.
640 ///
641 /// [`process_inbound_uni_with_close`]: Self::process_inbound_uni_with_close
642 ///
643 /// # Errors
644 ///
645 /// Returns a `H3Error` in case of io error or http/3 semantic error.
646 #[deprecated(
647 since = "1.2.0",
648 note = "use `process_inbound_uni_with_close` so connection-level protocol errors close \
649 the QUIC connection before the recv stream drops, avoiding a `FINAL_SIZE_ERROR` \
650 race with the peer's response to STOP_SENDING"
651 )]
652 pub async fn process_inbound_uni<T>(&self, stream: T) -> Result<UniStreamResult<T>, H3Error>
653 where
654 T: AsyncRead + Unpin + Send,
655 {
656 self.process_inbound_uni_with_close(stream, |_| {}).await
657 }
658
659 /// Handle an inbound unidirectional HTTP/3 stream from the peer, calling `on_close` to
660 /// close the QUIC connection if a connection-level protocol error is detected.
661 ///
662 /// Identical to [`process_inbound_uni`][Self::process_inbound_uni] except that on
663 /// any `H3Error::Protocol(code)` whose code is a connection-level error (RFC 9114,
664 /// RFC 9204), `on_close` is invoked with that code while the recv stream is still alive. This
665 /// lets callers send a `CONNECTION_CLOSE` before the stream drops — if the close call sets
666 /// quinn's `conn.error`, quinn's `RecvStream::drop` skips `STOP_SENDING`, eliminating a
667 /// peer race that otherwise causes `FINAL_SIZE_ERROR` to override the app error code.
668 ///
669 /// `on_close` is a `FnOnce` taking `H3ErrorCode`. trillium-http does not itself
670 /// hold the QUIC connection; callers wire up the actual `connection.close()` call
671 /// inside the closure (e.g. quinn's `Connection::close`).
672 ///
673 /// # Errors
674 ///
675 /// Returns a `H3Error` in case of io error or http/3 semantic error.
676 pub async fn process_inbound_uni_with_close<T, OnClose>(
677 &self,
678 mut stream: T,
679 on_close: OnClose,
680 ) -> Result<UniStreamResult<T>, H3Error>
681 where
682 T: AsyncRead + Unpin + Send,
683 OnClose: FnOnce(H3ErrorCode),
684 {
685 let inner = self
686 .swansong
687 .interrupt(self.process_inbound_uni_inner(&mut stream))
688 .await
689 .unwrap_or(Ok(UniInnerResult::Handled)); // interrupted
690
691 match inner {
692 Ok(UniInnerResult::Handled) => Ok(UniStreamResult::Handled),
693 Ok(UniInnerResult::WebTransport { session_id, buffer }) => {
694 Ok(UniStreamResult::WebTransport {
695 session_id,
696 stream,
697 buffer,
698 })
699 }
700 Ok(UniInnerResult::Unknown { stream_type }) => Ok(UniStreamResult::Unknown {
701 stream_type,
702 stream,
703 }),
704 Err(error) => {
705 // Fire `on_close` BEFORE returning so the caller's connection.close
706 // call sets quinn's `conn.error` while `stream` is still alive. When
707 // `stream` then drops at function return, quinn's `RecvStream::drop`
708 // skips STOP_SENDING — preventing the peer-RESET_STREAM race that
709 // otherwise replaces our app close code with FINAL_SIZE_ERROR.
710 if let H3Error::Protocol(code) = &error
711 && code.is_connection_error()
712 {
713 on_close(*code);
714 }
715 Err(error)
716 }
717 }
718 }
719
720 /// Inner-loop body of [`process_inbound_uni_with_close`][Self::process_inbound_uni_with_close].
721 /// Borrows `stream` so the outer function can keep ownership of it across the await,
722 /// which lets the caller's close callback fire before the recv stream drops.
723 async fn process_inbound_uni_inner<T>(&self, stream: &mut T) -> Result<UniInnerResult, H3Error>
724 where
725 T: AsyncRead + Unpin + Send,
726 {
727 let mut buf = vec![0; 128];
728 let mut filled = 0;
729
730 // Read stream type varint (decode as raw u64 to handle unknown types)
731 let stream_type = read(&mut buf, &mut filled, stream, |data| {
732 match quic_varint::decode(data) {
733 Ok(ok) => Ok(Some(ok)),
734 Err(QuicVarIntError::UnexpectedEnd) => Ok(None),
735 // this branch is unreachable because u64 is always From<u64>
736 Err(QuicVarIntError::UnknownValue { bytes, value }) => Ok(Some((value, bytes))),
737 }
738 })
739 .await?;
740
741 match UniStreamType::try_from(stream_type) {
742 Ok(UniStreamType::Control) => {
743 log::trace!("H3 inbound uni: control stream");
744 self.run_inbound_control(&mut buf, &mut filled, stream)
745 .await?;
746 Ok(UniInnerResult::Handled)
747 }
748
749 Ok(UniStreamType::QpackEncoder) => {
750 log::trace!("H3 inbound uni: QPACK encoder stream ({filled} bytes pre-read)");
751 let mut reader = Prepended {
752 head: &buf[..filled],
753 tail: stream,
754 };
755
756 log::trace!("QPACK encoder stream: started");
757 self.decoder_dynamic_table.run_reader(&mut reader).await?;
758
759 Ok(UniInnerResult::Handled)
760 }
761
762 Ok(UniStreamType::QpackDecoder) => {
763 log::trace!("H3 inbound uni: QPACK decoder stream ({filled} bytes pre-read)");
764 let mut reader = Prepended {
765 head: &buf[..filled],
766 tail: stream,
767 };
768 self.encoder_dynamic_table.run_reader(&mut reader).await?;
769 Ok(UniInnerResult::Handled)
770 }
771
772 Ok(UniStreamType::WebTransport) => {
773 log::trace!("H3 inbound uni: WebTransport stream");
774 let session_id =
775 read(
776 &mut buf,
777 &mut filled,
778 stream,
779 |data| match quic_varint::decode(data) {
780 Ok(ok) => Ok(Some(ok)),
781 Err(QuicVarIntError::UnexpectedEnd) => Ok(None),
782 Err(QuicVarIntError::UnknownValue { bytes, value }) => {
783 Ok(Some((value, bytes)))
784 }
785 },
786 )
787 .await?;
788
789 buf.truncate(filled);
790
791 Ok(UniInnerResult::WebTransport {
792 session_id,
793 buffer: buf.into(),
794 })
795 }
796
797 Ok(UniStreamType::Push) => {
798 // Trillium does not support HTTP/3 push, so we hand these back as `Unknown`
799 // identically to truly-unknown stream types — the explicit arm exists so
800 // trace output names "push stream" rather than a bare type id.
801 log::trace!("H3 inbound uni: push stream (push not supported)");
802 Ok(UniInnerResult::Unknown { stream_type })
803 }
804
805 Err(_) => {
806 log::trace!("H3 inbound uni: unknown stream type {stream_type:#x}");
807 Ok(UniInnerResult::Unknown { stream_type })
808 }
809 }
810 }
811
812 /// Handle the http/3 peer's inbound control stream.
813 ///
814 /// # Errors
815 ///
816 /// Returns a `H3Error` in case of io error or HTTP/3 semantic error.
817 async fn run_inbound_control<T>(
818 &self,
819 buf: &mut Vec<u8>,
820 filled: &mut usize,
821 stream: &mut T,
822 ) -> Result<(), H3Error>
823 where
824 T: AsyncRead + Unpin + Send,
825 {
826 // SettingsError takes priority: a SETTINGS frame whose payload is itself invalid
827 // (e.g. forbidden HTTP/2 setting IDs) is reported as SETTINGS_ERROR, not the
828 // MISSING_SETTINGS we report for everything else here.
829 let settings = read(buf, filled, stream, |data| match Frame::decode(data) {
830 Ok((Frame::Settings(s), consumed)) => Ok(Some((s, consumed))),
831 Err(FrameDecodeError::Incomplete) => Ok(None),
832 Err(FrameDecodeError::Error(H3ErrorCode::SettingsError)) => {
833 Err(H3ErrorCode::SettingsError)
834 }
835 Ok(_) | Err(FrameDecodeError::Error(_)) => Err(H3ErrorCode::MissingSettings),
836 })
837 .await
838 .map_err(map_critical_stream_eof)?;
839
840 log::trace!("H3 peer settings: {settings:?}");
841
842 self.peer_settings
843 .set(settings)
844 .map_err(|_| H3ErrorCode::FrameUnexpected)?;
845 self.wake_peer_settings_waiters();
846
847 self.encoder_dynamic_table
848 .initialize_from_peer_settings(settings);
849
850 loop {
851 let frame = self
852 .swansong
853 .interrupt(read(buf, filled, stream, |data| {
854 match Frame::decode(data) {
855 Ok((frame, consumed)) => Ok(Some((frame, consumed))),
856 Err(FrameDecodeError::Incomplete) => Ok(None),
857 Err(FrameDecodeError::Error(code)) => Err(code),
858 }
859 }))
860 .await
861 .transpose()
862 .map_err(map_critical_stream_eof)?;
863
864 match frame {
865 None => {
866 log::trace!("H3 control stream: interrupted by shutdown");
867 return Ok(());
868 }
869
870 Some(Frame::Goaway(id)) => {
871 log::trace!("H3 control stream: peer sent GOAWAY(stream_id={id})");
872 self.swansong.shut_down();
873 return Ok(());
874 }
875
876 Some(Frame::Unknown(n)) => {
877 // Consume the payload bytes so the stream stays synchronized.
878 log::trace!("H3 control stream: skipping unknown frame (payload {n} bytes)");
879 let n = usize::try_from(n).unwrap_or(usize::MAX);
880 let in_buf = n.min(*filled);
881 buf.copy_within(in_buf..*filled, 0);
882 *filled -= in_buf;
883 let mut todo = n - in_buf;
884 let mut scratch = [0u8; 256];
885 while todo > 0 {
886 let to_read = todo.min(scratch.len());
887 let n = stream
888 .read(&mut scratch[..to_read])
889 .await
890 .map_err(H3Error::Io)?;
891 if n == 0 {
892 return Err(H3ErrorCode::ClosedCriticalStream.into());
893 }
894 todo -= n;
895 }
896 }
897
898 Some(
899 Frame::Settings(_)
900 | Frame::Data(_)
901 | Frame::Headers(_)
902 | Frame::PushPromise { .. }
903 | Frame::WebTransport(_),
904 ) => {
905 return Err(H3ErrorCode::FrameUnexpected.into());
906 }
907
908 Some(Frame::PriorityUpdate {
909 prioritized_element_id,
910 priority,
911 }) => {
912 log::trace!(
913 "H3 control stream: PRIORITY_UPDATE stream={prioritized_element_id} \
914 priority=\"{priority}\""
915 );
916 self.emit_priority_update(prioritized_element_id, priority);
917 }
918
919 // Trillium doesn't implement push, so these are ignored rather than acted on.
920 Some(Frame::CancelPush(_) | Frame::MaxPushId(_)) => {
921 log::trace!("H3 control stream: ignoring {frame:?}");
922 }
923 }
924 }
925 }
926}
927
928/// A pending HTTP/3 request-response cycle on one bidirectional stream, with optional
929/// per-stream hooks.
930///
931/// Built by [`H3Connection::process_inbound_bidi`]. Configure hooks with the `with_*`
932/// methods and `.await` it to run the cycle. New per-stream extension points are added as
933/// further `with_*` methods, so the entry point's required arguments never change.
934pub struct H3BidiRequest<Transport, Handler> {
935 h3: Arc<H3Connection>,
936 transport: Transport,
937 handler: Handler,
938 stream_id: u64,
939 reset: Option<ResetHook<Transport>>,
940 reject_requests: bool,
941 peer_gone: Option<PeerGone>,
942}
943
944/// Per-stream reset hook: RST both halves with the still-owned transport on a stream-level error.
945type ResetHook<Transport> = Box<dyn FnOnce(&mut Transport, H3ErrorCode) + Send>;
946
947impl<Transport, Handler> std::fmt::Debug for H3BidiRequest<Transport, Handler> {
948 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949 f.debug_struct("H3BidiRequest")
950 .field("stream_id", &self.stream_id)
951 .finish_non_exhaustive()
952 }
953}
954
955impl<Transport, Handler> H3BidiRequest<Transport, Handler> {
956 /// Issue a stream RST on a stream-level protocol error.
957 ///
958 /// On any `H3Error::Protocol(code)` from first-frame processing, `reset` is called with
959 /// the still-owned transport and the error code before the error is returned — letting the
960 /// caller RST both halves of the bidi stream as RFC 9114 requires. I/O errors and
961 /// successful runs do not invoke it. Without this hook, the transport is dropped without a
962 /// reset.
963 #[must_use]
964 pub fn with_reset<R>(mut self, reset: R) -> Self
965 where
966 R: FnOnce(&mut Transport, H3ErrorCode) + Send + 'static,
967 {
968 self.reset = Some(Box::new(reset));
969 self
970 }
971
972 /// Report peer abandonment of this stream — `STOP_SENDING`, stream reset, or connection
973 /// loss — to [`Conn::is_disconnected`], [`Conn::cancel_on_disconnect`], and
974 /// [`Upgrade::poll_closed`][crate::Upgrade::poll_closed].
975 ///
976 /// QUIC signals departure out-of-band rather than through the byte stream, and an h3
977 /// client half-closes its send side as soon as the request is complete, so reading the
978 /// transport can neither detect abandonment nor be used as a proxy for it. Without this
979 /// hook those three APIs never report a disconnection on HTTP/3.
980 ///
981 /// The future must resolve only on abandonment: an h3 request stream is not finished by
982 /// the peer under normal operation, so a future that also resolves on orderly completion
983 /// (as `quinn::SendStream::stopped` does) is only correct if it is derived from the send
984 /// half of a stream this connection has not yet finished.
985 #[must_use]
986 pub fn with_peer_gone(mut self, peer_gone: PeerGone) -> Self {
987 self.peer_gone = Some(peer_gone);
988 self
989 }
990
991 /// Treat an HTTP request on this stream as a protocol violation instead of running the
992 /// handler.
993 ///
994 /// A peer that accepts inbound bidirectional streams only for negotiated extensions —
995 /// an HTTP/3 client, where RFC 9114 makes a server-initiated request stream a connection
996 /// error — enables this to refuse requests without responding to them. When the stream's
997 /// first frame begins an HTTP request, the handler is skipped, nothing is written to the
998 /// stream, the [`with_reset`][Self::with_reset] hook is invoked with
999 /// [`H3ErrorCode::StreamCreationError`], and awaiting resolves to that error. Closing the
1000 /// connection is the caller's responsibility, typically inside the reset hook while the
1001 /// stream is still alive.
1002 #[must_use]
1003 pub fn with_request_rejection(mut self) -> Self {
1004 self.reject_requests = true;
1005 self
1006 }
1007}
1008
1009impl<Transport, Handler, Fut> IntoFuture for H3BidiRequest<Transport, Handler>
1010where
1011 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
1012 Handler: FnOnce(Conn<Transport>) -> Fut + Send + 'static,
1013 Fut: Future<Output = Conn<Transport>> + Send + 'static,
1014{
1015 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1016 type Output = Result<H3StreamResult<Transport>, H3Error>;
1017
1018 fn into_future(self) -> Self::IntoFuture {
1019 Box::pin(async move {
1020 let Self {
1021 h3,
1022 mut transport,
1023 handler,
1024 stream_id,
1025 reset,
1026 reject_requests,
1027 peer_gone,
1028 } = self;
1029
1030 h3.record_accepted_stream(stream_id);
1031 let _guard = h3.swansong.guard();
1032 let mut buffer: Buffer =
1033 Vec::with_capacity(h3.context.config.request_buffer_initial_len).into();
1034
1035 let outcome =
1036 Conn::process_first_frame_h3(&h3, &mut transport, &mut buffer, stream_id).await;
1037
1038 match outcome {
1039 Ok(H3FirstFrame::Request { .. }) if reject_requests => {
1040 let code = H3ErrorCode::StreamCreationError;
1041 if let Some(reset) = reset {
1042 reset(&mut transport, code);
1043 }
1044 Err(code.into())
1045 }
1046 Ok(H3FirstFrame::Request {
1047 validated,
1048 start_time,
1049 }) => {
1050 let initial_priority = validated
1051 .request_headers
1052 .get_str(KnownHeaderName::Priority)
1053 .map_or_default(Priority::parse);
1054 h3.emit_priority(stream_id, initial_priority, false);
1055 let conn = Conn::build_h3(
1056 h3, transport, buffer, validated, start_time, stream_id, peer_gone,
1057 );
1058 Ok(H3StreamResult::Request(
1059 handler(conn).await.send_h3().await?,
1060 ))
1061 }
1062 Ok(H3FirstFrame::WebTransport { session_id }) => Ok(H3StreamResult::WebTransport {
1063 session_id,
1064 transport,
1065 buffer,
1066 }),
1067 Err(error) => {
1068 if let H3Error::Protocol(code) = &error
1069 && let Some(reset) = reset
1070 {
1071 reset(&mut transport, *code);
1072 }
1073 Err(error)
1074 }
1075 }
1076 })
1077 }
1078}
1079
1080/// Map an `UnexpectedEof` I/O error (the `read` helper's "stream FIN'd" signal) to
1081/// `H3_CLOSED_CRITICAL_STREAM`. Closure of the control stream or of either QPACK
1082/// side-channel is a connection error. Other I/O errors and any protocol error are passed
1083/// through unchanged.
1084fn map_critical_stream_eof(error: H3Error) -> H3Error {
1085 match error {
1086 H3Error::Io(e) if e.kind() == ErrorKind::UnexpectedEof => {
1087 H3ErrorCode::ClosedCriticalStream.into()
1088 }
1089 other => other,
1090 }
1091}
1092
1093async fn write(
1094 buf: &mut Vec<u8>,
1095 mut stream: impl AsyncWrite + Unpin + Send,
1096 mut f: impl FnMut(&mut [u8]) -> Option<usize>,
1097) -> io::Result<usize> {
1098 let written = loop {
1099 if let Some(w) = f(buf) {
1100 break w;
1101 }
1102 if buf.len() >= MAX_BUFFER_SIZE {
1103 return Err(io::Error::new(ErrorKind::OutOfMemory, "runaway allocation"));
1104 }
1105 buf.resize(buf.len() * 2, 0);
1106 };
1107
1108 stream.write_all(&buf[..written]).await?;
1109 stream.flush().await?;
1110 Ok(written)
1111}
1112
1113/// An `AsyncRead` adapter that drains a byte slice before reading from an inner stream.
1114///
1115/// Used to replay bytes that were read ahead while parsing a stream-type varint, before
1116/// dispatching to the inner runner that consumes the rest of the stream.
1117struct Prepended<'a, T> {
1118 head: &'a [u8],
1119 tail: T,
1120}
1121
1122impl<T: AsyncRead + Unpin> AsyncRead for Prepended<'_, T> {
1123 fn poll_read(
1124 self: Pin<&mut Self>,
1125 cx: &mut Context<'_>,
1126 out: &mut [u8],
1127 ) -> Poll<io::Result<usize>> {
1128 let this = self.get_mut();
1129 if !this.head.is_empty() {
1130 let n = this.head.len().min(out.len());
1131 out[..n].copy_from_slice(&this.head[..n]);
1132 this.head = &this.head[n..];
1133 return Poll::Ready(Ok(n));
1134 }
1135 Pin::new(&mut this.tail).poll_read(cx, out)
1136 }
1137}
1138
1139/// Read from `stream` into `buf` until `f` can decode a value.
1140///
1141/// `f` receives the filled portion of the buffer and returns:
1142/// - `Ok(Some((value, consumed)))` — success; consumed bytes are removed from the front
1143/// - `Ok(None)` — need more data; reads more bytes and retries
1144/// - `Err(e)` — unrecoverable error; propagated to caller
1145async fn read<R>(
1146 buf: &mut Vec<u8>,
1147 filled: &mut usize,
1148 stream: &mut (impl AsyncRead + Unpin + Send),
1149 f: impl Fn(&[u8]) -> Result<Option<(R, usize)>, H3ErrorCode>,
1150) -> Result<R, H3Error> {
1151 loop {
1152 if let Some((result, consumed)) = f(&buf[..*filled])? {
1153 buf.copy_within(consumed..*filled, 0);
1154 *filled -= consumed;
1155 return Ok(result);
1156 }
1157
1158 if *filled >= buf.len() {
1159 if buf.len() >= MAX_BUFFER_SIZE {
1160 return Err(io::Error::new(ErrorKind::OutOfMemory, "runaway allocation").into());
1161 }
1162 buf.resize(buf.len() * 2, 0);
1163 }
1164
1165 let n = stream.read(&mut buf[*filled..]).await?;
1166 if n == 0 {
1167 return Err(io::Error::new(ErrorKind::UnexpectedEof, "stream closed").into());
1168 }
1169 *filled += n;
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests;