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