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};
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 peer_gone: None,
332 }
333 }
334
335 /// Process a single HTTP/3 request-response cycle on a bidirectional stream, calling
336 /// `reset` to issue a stream RST when a stream-level protocol error occurs.
337 ///
338 /// On any `H3Error::Protocol(code)` produced by first-frame processing (HEADERS decode,
339 /// pseudo-header validation, etc.), `reset` is invoked with the still-owned transport and
340 /// the error code before the error is returned. This lets callers RST both the recv and
341 /// send halves of the bidi stream — required by RFC 9114 for stream errors like
342 /// `H3_MESSAGE_ERROR`. I/O errors and successful runs do not invoke `reset`.
343 ///
344 /// `reset` is a `FnOnce` taking `(&mut Transport, H3ErrorCode)`. trillium-http does not
345 /// itself depend on any reset capability of the transport; callers wire up the actual
346 /// stream-RST mechanism (e.g. quinn's `RecvStream::stop` + `SendStream::reset`) inside
347 /// the closure.
348 ///
349 /// # Errors
350 ///
351 /// Returns an `H3Error` in case of io error or http/3 semantic error.
352 // This is not deprecated yet because it didn't make sense to release a new version of
353 // trillium-client just to avoid this deprecation, but the intention is to deprecate
354 pub async fn process_inbound_bidi_with_reset<Transport, Handler, Fut, Reset>(
355 self: Arc<Self>,
356 mut transport: Transport,
357 handler: Handler,
358 stream_id: u64,
359 reset: Reset,
360 ) -> Result<H3StreamResult<Transport>, H3Error>
361 where
362 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
363 Handler: FnOnce(Conn<Transport>) -> Fut,
364 Fut: Future<Output = Conn<Transport>>,
365 Reset: FnOnce(&mut Transport, H3ErrorCode),
366 {
367 self.record_accepted_stream(stream_id);
368 let _guard = self.swansong.guard();
369 let mut buffer: Buffer =
370 Vec::with_capacity(self.context.config.request_buffer_initial_len).into();
371
372 let outcome =
373 Conn::process_first_frame_h3(&self, &mut transport, &mut buffer, stream_id).await;
374
375 match outcome {
376 Ok(H3FirstFrame::Request {
377 validated,
378 start_time,
379 }) => {
380 let conn = Conn::build_h3(
381 self, transport, buffer, validated, start_time, stream_id, None,
382 );
383 Ok(H3StreamResult::Request(
384 handler(conn).await.send_h3().await?,
385 ))
386 }
387 Ok(H3FirstFrame::WebTransport { session_id }) => Ok(H3StreamResult::WebTransport {
388 session_id,
389 transport,
390 buffer,
391 }),
392 Err(error) => {
393 if let H3Error::Protocol(code) = &error {
394 reset(&mut transport, *code);
395 }
396 Err(error)
397 }
398 }
399 }
400
401 /// Decode a QPACK-encoded field section, consulting the dynamic table as needed.
402 ///
403 /// If the field section's Required Insert Count is greater than zero, waits until the
404 /// dynamic table has received enough entries. Returns an error on protocol violations or
405 /// if the encoder stream fails while waiting.
406 ///
407 /// Duplicate pseudo-headers are silently ignored (first value wins). Unknown
408 /// pseudo-headers are rejected.
409 ///
410 /// # Errors
411 ///
412 /// Returns an error if the encoded bytes cannot be parsed as a valid field section.
413 #[cfg(feature = "unstable")]
414 pub async fn decode_field_section(
415 &self,
416 encoded: &[u8],
417 stream_id: u64,
418 ) -> Result<FieldSection<'static>, H3Error> {
419 self.decoder_dynamic_table.decode(encoded, stream_id).await
420 }
421
422 #[cfg(not(feature = "unstable"))]
423 pub(crate) async fn decode_field_section(
424 &self,
425 encoded: &[u8],
426 stream_id: u64,
427 ) -> Result<FieldSection<'static>, H3Error> {
428 self.decoder_dynamic_table.decode(encoded, stream_id).await
429 }
430
431 /// Encode a QPACK field section (no HTTP/3 framing) from pseudo-headers and headers,
432 /// consulting the encoder dynamic table to emit literal-with-name-reference or indexed
433 /// representations as the table's contents allow.
434 ///
435 /// Superseded by [`encode_field_section_framed`][Self::encode_field_section_framed], which
436 /// also frames the section and enforces the peer's field-section size limit.
437 ///
438 /// # Errors
439 ///
440 /// Returns an `H3Error` in case of http/3 semantic error.
441 // Retained only so an older `trillium-client` that predates the framed method still builds
442 // against this crate; remove it at the next breaking release.
443 #[cfg(feature = "unstable")]
444 #[allow(clippy::unnecessary_wraps, reason = "future-proofing api")]
445 pub fn encode_field_section(
446 &self,
447 field_section: &FieldSection<'_>,
448 buf: &mut Vec<u8>,
449 stream_id: u64,
450 ) -> Result<(), H3Error> {
451 self.encoder_dynamic_table
452 .encode(field_section, buf, stream_id);
453 Ok(())
454 }
455
456 /// Encode `field_section` as a complete HTTP/3 HEADERS frame — the QPACK-compressed field
457 /// section prefixed with its `type` + `length` frame header — and append it to `buffer`.
458 ///
459 /// If the peer's `SETTINGS_MAX_FIELD_SECTION_SIZE` is known and the section's
460 /// [`uncompressed_len`][crate::headers::FieldSection] exceeds it, the section is rejected
461 /// before encoding and nothing is written. Until the peer's SETTINGS arrive the limit is
462 /// unknown and unenforced (RFC 9114 §4.2.2).
463 ///
464 /// # Errors
465 ///
466 /// [`io::ErrorKind::InvalidData`] if the field section exceeds the peer's advertised limit, or
467 /// the QPACK encoder's error mapped through [`io::Error::other`].
468 #[cfg(feature = "unstable")]
469 #[doc(hidden)]
470 pub fn encode_field_section_framed(
471 &self,
472 field_section: &FieldSection<'_>,
473 buffer: &mut Vec<u8>,
474 stream_id: u64,
475 ) -> io::Result<()> {
476 self.encode_field_section_framed_impl(field_section, buffer, stream_id)
477 }
478
479 #[cfg(not(feature = "unstable"))]
480 pub(crate) fn encode_field_section_framed(
481 &self,
482 field_section: &FieldSection<'_>,
483 buffer: &mut Vec<u8>,
484 stream_id: u64,
485 ) -> io::Result<()> {
486 self.encode_field_section_framed_impl(field_section, buffer, stream_id)
487 }
488
489 fn encode_field_section_framed_impl(
490 &self,
491 field_section: &FieldSection<'_>,
492 buffer: &mut Vec<u8>,
493 stream_id: u64,
494 ) -> io::Result<()> {
495 // Enforce the peer's SETTINGS_MAX_FIELD_SECTION_SIZE against the uncompressed size the
496 // limit is defined in (RFC 9114 §4.2.2), before encoding — an over-limit section is
497 // rejected without being written to the wire.
498 if let Some(max_size) = self
499 .peer_settings()
500 .and_then(H3Settings::max_field_section_size)
501 {
502 let size = field_section.uncompressed_len();
503 if size > max_size {
504 return Err(io::Error::new(
505 ErrorKind::InvalidData,
506 format!("field section would be longer than peer allows ({size} > {max_size})"),
507 ));
508 }
509 }
510
511 let start = buffer.len();
512 self.encoder_dynamic_table
513 .encode(field_section, buffer, stream_id);
514
515 // The HEADERS frame length is the encoded (compressed) payload length, distinct from the
516 // uncompressed size enforced above. Encode the section in place, then open a gap in front
517 // of it for the now-known frame header and shift the section right into place.
518 let section_len = buffer.len() - start;
519 let frame = Frame::Headers(section_len as u64);
520 let frame_header_len = frame.encoded_len();
521 buffer.resize(buffer.len() + frame_header_len, 0);
522 buffer.copy_within(start..start + section_len, start + frame_header_len);
523 frame.encode(&mut buffer[start..start + frame_header_len]);
524 Ok(())
525 }
526
527 /// Run this connection's HTTP/3 outbound control stream.
528 ///
529 /// Sends the initial SETTINGS frame, then sends GOAWAY when the connection shuts down.
530 /// Returns after GOAWAY is sent; keep the stream open until the QUIC connection closes
531 /// (closing a control stream is a connection error).
532 ///
533 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return, for the same reason
534 /// as [`run_encoder`][Self::run_encoder].
535 ///
536 /// # Errors
537 ///
538 /// Returns an `H3Error` in case of io error or http/3 semantic error.
539 pub async fn run_outbound_control<T>(&self, mut stream: T) -> Result<(), H3Error>
540 where
541 T: AsyncWrite + Unpin + Send,
542 {
543 let result: Result<(), H3Error> = async {
544 let mut buf = vec![0; 128];
545
546 let settings = Frame::Settings(H3Settings::from(&self.context.config));
547 log::trace!(
548 "H3 outbound control: sending SETTINGS: {:?}",
549 H3Settings::from(&self.context.config)
550 );
551
552 write(&mut buf, &mut stream, |buf| {
553 let mut written = quic_varint::encode(UniStreamType::Control, buf)?;
554 written += settings.encode(&mut buf[written..])?;
555 Some(written)
556 })
557 .await?;
558 log::trace!("H3 outbound control: SETTINGS sent");
559
560 self.swansong.clone().await;
561
562 write(&mut buf, &mut stream, |buf| {
563 Frame::Goaway(self.goaway_id()).encode(buf)
564 })
565 .await?;
566
567 Ok(())
568 }
569 .await;
570
571 self.shut_down();
572 result
573 }
574
575 /// Run the outbound QPACK encoder stream for the duration of the connection.
576 ///
577 /// Writes the stream type byte, then drains encoder-stream instructions from the encoder
578 /// dynamic table as they are enqueued. Returns when the connection shuts down or the table is
579 /// marked failed.
580 ///
581 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return. This stream is
582 /// mandatory for the connection's lifetime, so its termination — clean or errored — means the
583 /// connection can no longer function; marking it shut down lets a pooling caller evict it
584 /// rather than hand it back out. Idempotent on the clean path, which returns only after
585 /// shutdown has already begun.
586 ///
587 /// # Errors
588 ///
589 /// Returns an `H3Error` in case of io error.
590 pub async fn run_encoder<T>(&self, mut stream: T) -> Result<(), H3Error>
591 where
592 T: AsyncWrite + Unpin + Send,
593 {
594 let result = self
595 .encoder_dynamic_table
596 .run_writer(&mut stream, self.swansong.clone())
597 .await;
598 self.shut_down();
599 result
600 }
601
602 /// Run the outbound QPACK decoder stream for the duration of the connection.
603 ///
604 /// Writes the stream type byte, then loops sending Section Acknowledgement and Insert
605 /// Count Increment instructions as they become needed. Returns when the connection
606 /// shuts down.
607 ///
608 /// Shuts the connection down ([`shut_down`][Self::shut_down]) on return, for the same reason
609 /// as [`run_encoder`][Self::run_encoder].
610 ///
611 /// # Errors
612 ///
613 /// Returns an `H3Error` in case of io error or http/3 semantic error.
614 pub async fn run_decoder<T>(&self, mut stream: T) -> Result<(), H3Error>
615 where
616 T: AsyncWrite + Unpin + Send,
617 {
618 let result = self
619 .decoder_dynamic_table
620 .run_writer(&mut stream, self.swansong.clone())
621 .await;
622 self.shut_down();
623 result
624 }
625
626 /// Handle an inbound unidirectional HTTP/3 stream from the peer.
627 ///
628 /// Internal stream types (control, QPACK encoder/decoder) are handled automatically;
629 /// application streams are returned via [`UniStreamResult`] for the caller to process.
630 ///
631 /// On a connection-level protocol error, this method drops the recv stream before
632 /// the caller can react. Quinn's `RecvStream::drop` then sends `STOP_SENDING`, which
633 /// races against the caller's `connection.close` — if the peer responds with a
634 /// malformed `RESET_STREAM` (notably `final_offset = 0`) before our app close is
635 /// applied, the transport-level error overrides our app error code on the wire.
636 /// Use [`process_inbound_uni_with_close`] to thread the close call through the
637 /// function so it fires before the stream drops.
638 ///
639 /// [`process_inbound_uni_with_close`]: Self::process_inbound_uni_with_close
640 ///
641 /// # Errors
642 ///
643 /// Returns a `H3Error` in case of io error or http/3 semantic error.
644 #[deprecated(
645 since = "1.2.0",
646 note = "use `process_inbound_uni_with_close` so connection-level protocol errors close \
647 the QUIC connection before the recv stream drops, avoiding a `FINAL_SIZE_ERROR` \
648 race with the peer's response to STOP_SENDING"
649 )]
650 pub async fn process_inbound_uni<T>(&self, stream: T) -> Result<UniStreamResult<T>, H3Error>
651 where
652 T: AsyncRead + Unpin + Send,
653 {
654 self.process_inbound_uni_with_close(stream, |_| {}).await
655 }
656
657 /// Handle an inbound unidirectional HTTP/3 stream from the peer, calling `on_close` to
658 /// close the QUIC connection if a connection-level protocol error is detected.
659 ///
660 /// Identical to [`process_inbound_uni`][Self::process_inbound_uni] except that on
661 /// any `H3Error::Protocol(code)` whose code is a connection-level error (RFC 9114,
662 /// RFC 9204), `on_close` is invoked with that code while the recv stream is still alive. This
663 /// lets callers send a `CONNECTION_CLOSE` before the stream drops — if the close call sets
664 /// quinn's `conn.error`, quinn's `RecvStream::drop` skips `STOP_SENDING`, eliminating a
665 /// peer race that otherwise causes `FINAL_SIZE_ERROR` to override the app error code.
666 ///
667 /// `on_close` is a `FnOnce` taking `H3ErrorCode`. trillium-http does not itself
668 /// hold the QUIC connection; callers wire up the actual `connection.close()` call
669 /// inside the closure (e.g. quinn's `Connection::close`).
670 ///
671 /// # Errors
672 ///
673 /// Returns a `H3Error` in case of io error or http/3 semantic error.
674 pub async fn process_inbound_uni_with_close<T, OnClose>(
675 &self,
676 mut stream: T,
677 on_close: OnClose,
678 ) -> Result<UniStreamResult<T>, H3Error>
679 where
680 T: AsyncRead + Unpin + Send,
681 OnClose: FnOnce(H3ErrorCode),
682 {
683 let inner = self
684 .swansong
685 .interrupt(self.process_inbound_uni_inner(&mut stream))
686 .await
687 .unwrap_or(Ok(UniInnerResult::Handled)); // interrupted
688
689 match inner {
690 Ok(UniInnerResult::Handled) => Ok(UniStreamResult::Handled),
691 Ok(UniInnerResult::WebTransport { session_id, buffer }) => {
692 Ok(UniStreamResult::WebTransport {
693 session_id,
694 stream,
695 buffer,
696 })
697 }
698 Ok(UniInnerResult::Unknown { stream_type }) => Ok(UniStreamResult::Unknown {
699 stream_type,
700 stream,
701 }),
702 Err(error) => {
703 // Fire `on_close` BEFORE returning so the caller's connection.close
704 // call sets quinn's `conn.error` while `stream` is still alive. When
705 // `stream` then drops at function return, quinn's `RecvStream::drop`
706 // skips STOP_SENDING — preventing the peer-RESET_STREAM race that
707 // otherwise replaces our app close code with FINAL_SIZE_ERROR.
708 if let H3Error::Protocol(code) = &error
709 && code.is_connection_error()
710 {
711 on_close(*code);
712 }
713 Err(error)
714 }
715 }
716 }
717
718 /// Inner-loop body of [`process_inbound_uni_with_close`][Self::process_inbound_uni_with_close].
719 /// Borrows `stream` so the outer function can keep ownership of it across the await,
720 /// which lets the caller's close callback fire before the recv stream drops.
721 async fn process_inbound_uni_inner<T>(&self, stream: &mut T) -> Result<UniInnerResult, H3Error>
722 where
723 T: AsyncRead + Unpin + Send,
724 {
725 let mut buf = vec![0; 128];
726 let mut filled = 0;
727
728 // Read stream type varint (decode as raw u64 to handle unknown types)
729 let stream_type = read(&mut buf, &mut filled, stream, |data| {
730 match quic_varint::decode(data) {
731 Ok(ok) => Ok(Some(ok)),
732 Err(QuicVarIntError::UnexpectedEnd) => Ok(None),
733 // this branch is unreachable because u64 is always From<u64>
734 Err(QuicVarIntError::UnknownValue { bytes, value }) => Ok(Some((value, bytes))),
735 }
736 })
737 .await?;
738
739 match UniStreamType::try_from(stream_type) {
740 Ok(UniStreamType::Control) => {
741 log::trace!("H3 inbound uni: control stream");
742 self.run_inbound_control(&mut buf, &mut filled, stream)
743 .await?;
744 Ok(UniInnerResult::Handled)
745 }
746
747 Ok(UniStreamType::QpackEncoder) => {
748 log::trace!("H3 inbound uni: QPACK encoder stream ({filled} bytes pre-read)");
749 let mut reader = Prepended {
750 head: &buf[..filled],
751 tail: stream,
752 };
753
754 log::trace!("QPACK encoder stream: started");
755 self.decoder_dynamic_table.run_reader(&mut reader).await?;
756
757 Ok(UniInnerResult::Handled)
758 }
759
760 Ok(UniStreamType::QpackDecoder) => {
761 log::trace!("H3 inbound uni: QPACK decoder stream ({filled} bytes pre-read)");
762 let mut reader = Prepended {
763 head: &buf[..filled],
764 tail: stream,
765 };
766 self.encoder_dynamic_table.run_reader(&mut reader).await?;
767 Ok(UniInnerResult::Handled)
768 }
769
770 Ok(UniStreamType::WebTransport) => {
771 log::trace!("H3 inbound uni: WebTransport stream");
772 let session_id =
773 read(
774 &mut buf,
775 &mut filled,
776 stream,
777 |data| match quic_varint::decode(data) {
778 Ok(ok) => Ok(Some(ok)),
779 Err(QuicVarIntError::UnexpectedEnd) => Ok(None),
780 Err(QuicVarIntError::UnknownValue { bytes, value }) => {
781 Ok(Some((value, bytes)))
782 }
783 },
784 )
785 .await?;
786
787 buf.truncate(filled);
788
789 Ok(UniInnerResult::WebTransport {
790 session_id,
791 buffer: buf.into(),
792 })
793 }
794
795 Ok(UniStreamType::Push) => {
796 // Trillium does not support HTTP/3 push, so we hand these back as `Unknown`
797 // identically to truly-unknown stream types — the explicit arm exists so
798 // trace output names "push stream" rather than a bare type id.
799 log::trace!("H3 inbound uni: push stream (push not supported)");
800 Ok(UniInnerResult::Unknown { stream_type })
801 }
802
803 Err(_) => {
804 log::trace!("H3 inbound uni: unknown stream type {stream_type:#x}");
805 Ok(UniInnerResult::Unknown { stream_type })
806 }
807 }
808 }
809
810 /// Handle the http/3 peer's inbound control stream.
811 ///
812 /// # Errors
813 ///
814 /// Returns a `H3Error` in case of io error or HTTP/3 semantic error.
815 async fn run_inbound_control<T>(
816 &self,
817 buf: &mut Vec<u8>,
818 filled: &mut usize,
819 stream: &mut T,
820 ) -> Result<(), H3Error>
821 where
822 T: AsyncRead + Unpin + Send,
823 {
824 // SettingsError takes priority: a SETTINGS frame whose payload is itself invalid
825 // (e.g. forbidden HTTP/2 setting IDs) is reported as SETTINGS_ERROR, not the
826 // MISSING_SETTINGS we report for everything else here.
827 let settings = read(buf, filled, stream, |data| match Frame::decode(data) {
828 Ok((Frame::Settings(s), consumed)) => Ok(Some((s, consumed))),
829 Err(FrameDecodeError::Incomplete) => Ok(None),
830 Err(FrameDecodeError::Error(H3ErrorCode::SettingsError)) => {
831 Err(H3ErrorCode::SettingsError)
832 }
833 Ok(_) | Err(FrameDecodeError::Error(_)) => Err(H3ErrorCode::MissingSettings),
834 })
835 .await
836 .map_err(map_critical_stream_eof)?;
837
838 log::trace!("H3 peer settings: {settings:?}");
839
840 self.peer_settings
841 .set(settings)
842 .map_err(|_| H3ErrorCode::FrameUnexpected)?;
843 self.wake_peer_settings_waiters();
844
845 self.encoder_dynamic_table
846 .initialize_from_peer_settings(settings);
847
848 loop {
849 let frame = self
850 .swansong
851 .interrupt(read(buf, filled, stream, |data| {
852 match Frame::decode(data) {
853 Ok((frame, consumed)) => Ok(Some((frame, consumed))),
854 Err(FrameDecodeError::Incomplete) => Ok(None),
855 Err(FrameDecodeError::Error(code)) => Err(code),
856 }
857 }))
858 .await
859 .transpose()
860 .map_err(map_critical_stream_eof)?;
861
862 match frame {
863 None => {
864 log::trace!("H3 control stream: interrupted by shutdown");
865 return Ok(());
866 }
867
868 Some(Frame::Goaway(id)) => {
869 log::trace!("H3 control stream: peer sent GOAWAY(stream_id={id})");
870 self.swansong.shut_down();
871 return Ok(());
872 }
873
874 Some(Frame::Unknown(n)) => {
875 // Consume the payload bytes so the stream stays synchronized.
876 log::trace!("H3 control stream: skipping unknown frame (payload {n} bytes)");
877 let n = usize::try_from(n).unwrap_or(usize::MAX);
878 let in_buf = n.min(*filled);
879 buf.copy_within(in_buf..*filled, 0);
880 *filled -= in_buf;
881 let mut todo = n - in_buf;
882 let mut scratch = [0u8; 256];
883 while todo > 0 {
884 let to_read = todo.min(scratch.len());
885 let n = stream
886 .read(&mut scratch[..to_read])
887 .await
888 .map_err(H3Error::Io)?;
889 if n == 0 {
890 return Err(H3ErrorCode::ClosedCriticalStream.into());
891 }
892 todo -= n;
893 }
894 }
895
896 Some(
897 Frame::Settings(_)
898 | Frame::Data(_)
899 | Frame::Headers(_)
900 | Frame::PushPromise { .. }
901 | Frame::WebTransport(_),
902 ) => {
903 return Err(H3ErrorCode::FrameUnexpected.into());
904 }
905
906 Some(Frame::PriorityUpdate {
907 prioritized_element_id,
908 priority,
909 }) => {
910 log::trace!(
911 "H3 control stream: PRIORITY_UPDATE stream={prioritized_element_id} \
912 priority=\"{priority}\""
913 );
914 self.emit_priority_update(prioritized_element_id, priority);
915 }
916
917 // Trillium doesn't implement push, so these are ignored rather than acted on.
918 Some(Frame::CancelPush(_) | Frame::MaxPushId(_)) => {
919 log::trace!("H3 control stream: ignoring {frame:?}");
920 }
921 }
922 }
923 }
924}
925
926/// A pending HTTP/3 request-response cycle on one bidirectional stream, with optional
927/// per-stream hooks.
928///
929/// Built by [`H3Connection::process_inbound_bidi`]. Configure hooks with the `with_*`
930/// methods and `.await` it to run the cycle. New per-stream extension points are added as
931/// further `with_*` methods, so the entry point's required arguments never change.
932pub struct H3BidiRequest<Transport, Handler> {
933 h3: Arc<H3Connection>,
934 transport: Transport,
935 handler: Handler,
936 stream_id: u64,
937 reset: Option<ResetHook<Transport>>,
938 reject_requests: bool,
939 peer_gone: Option<PeerGone>,
940}
941
942/// Per-stream reset hook: RST both halves with the still-owned transport on a stream-level error.
943type ResetHook<Transport> = Box<dyn FnOnce(&mut Transport, H3ErrorCode) + Send>;
944
945impl<Transport, Handler> std::fmt::Debug for H3BidiRequest<Transport, Handler> {
946 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947 f.debug_struct("H3BidiRequest")
948 .field("stream_id", &self.stream_id)
949 .finish_non_exhaustive()
950 }
951}
952
953impl<Transport, Handler> H3BidiRequest<Transport, Handler> {
954 /// Issue a stream RST on a stream-level protocol error.
955 ///
956 /// On any `H3Error::Protocol(code)` from first-frame processing, `reset` is called with
957 /// the still-owned transport and the error code before the error is returned — letting the
958 /// caller RST both halves of the bidi stream as RFC 9114 requires. I/O errors and
959 /// successful runs do not invoke it. Without this hook, the transport is dropped without a
960 /// reset.
961 #[must_use]
962 pub fn with_reset<R>(mut self, reset: R) -> Self
963 where
964 R: FnOnce(&mut Transport, H3ErrorCode) + Send + 'static,
965 {
966 self.reset = Some(Box::new(reset));
967 self
968 }
969
970 /// Report peer abandonment of this stream — `STOP_SENDING`, stream reset, or connection
971 /// loss — to [`Conn::is_disconnected`], [`Conn::cancel_on_disconnect`], and
972 /// [`Upgrade::poll_closed`][crate::Upgrade::poll_closed].
973 ///
974 /// QUIC signals departure out-of-band rather than through the byte stream, and an h3
975 /// client half-closes its send side as soon as the request is complete, so reading the
976 /// transport can neither detect abandonment nor be used as a proxy for it. Without this
977 /// hook those three APIs never report a disconnection on HTTP/3.
978 ///
979 /// The future must resolve only on abandonment: an h3 request stream is not finished by
980 /// the peer under normal operation, so a future that also resolves on orderly completion
981 /// (as `quinn::SendStream::stopped` does) is only correct if it is derived from the send
982 /// half of a stream this connection has not yet finished.
983 #[must_use]
984 pub fn with_peer_gone(mut self, peer_gone: PeerGone) -> Self {
985 self.peer_gone = Some(peer_gone);
986 self
987 }
988
989 /// Treat an HTTP request on this stream as a protocol violation instead of running the
990 /// handler.
991 ///
992 /// A peer that accepts inbound bidirectional streams only for negotiated extensions —
993 /// an HTTP/3 client, where RFC 9114 makes a server-initiated request stream a connection
994 /// error — enables this to refuse requests without responding to them. When the stream's
995 /// first frame begins an HTTP request, the handler is skipped, nothing is written to the
996 /// stream, the [`with_reset`][Self::with_reset] hook is invoked with
997 /// [`H3ErrorCode::StreamCreationError`], and awaiting resolves to that error. Closing the
998 /// connection is the caller's responsibility, typically inside the reset hook while the
999 /// stream is still alive.
1000 #[must_use]
1001 pub fn with_request_rejection(mut self) -> Self {
1002 self.reject_requests = true;
1003 self
1004 }
1005}
1006
1007impl<Transport, Handler, Fut> IntoFuture for H3BidiRequest<Transport, Handler>
1008where
1009 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
1010 Handler: FnOnce(Conn<Transport>) -> Fut + Send + 'static,
1011 Fut: Future<Output = Conn<Transport>> + Send + 'static,
1012{
1013 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1014 type Output = Result<H3StreamResult<Transport>, H3Error>;
1015
1016 fn into_future(self) -> Self::IntoFuture {
1017 Box::pin(async move {
1018 let Self {
1019 h3,
1020 mut transport,
1021 handler,
1022 stream_id,
1023 reset,
1024 reject_requests,
1025 peer_gone,
1026 } = self;
1027
1028 h3.record_accepted_stream(stream_id);
1029 let _guard = h3.swansong.guard();
1030 let mut buffer: Buffer =
1031 Vec::with_capacity(h3.context.config.request_buffer_initial_len).into();
1032
1033 let outcome =
1034 Conn::process_first_frame_h3(&h3, &mut transport, &mut buffer, stream_id).await;
1035
1036 match outcome {
1037 Ok(H3FirstFrame::Request { .. }) if reject_requests => {
1038 let code = H3ErrorCode::StreamCreationError;
1039 if let Some(reset) = reset {
1040 reset(&mut transport, code);
1041 }
1042 Err(code.into())
1043 }
1044 Ok(H3FirstFrame::Request {
1045 validated,
1046 start_time,
1047 }) => {
1048 let initial_priority = validated
1049 .request_headers
1050 .get_str(KnownHeaderName::Priority)
1051 .map(Priority::parse)
1052 .unwrap_or_default();
1053 h3.emit_priority(stream_id, initial_priority, false);
1054 let conn = Conn::build_h3(
1055 h3, transport, buffer, validated, start_time, stream_id, peer_gone,
1056 );
1057 Ok(H3StreamResult::Request(
1058 handler(conn).await.send_h3().await?,
1059 ))
1060 }
1061 Ok(H3FirstFrame::WebTransport { session_id }) => Ok(H3StreamResult::WebTransport {
1062 session_id,
1063 transport,
1064 buffer,
1065 }),
1066 Err(error) => {
1067 if let H3Error::Protocol(code) = &error
1068 && let Some(reset) = reset
1069 {
1070 reset(&mut transport, *code);
1071 }
1072 Err(error)
1073 }
1074 }
1075 })
1076 }
1077}
1078
1079/// Map an `UnexpectedEof` I/O error (the `read` helper's "stream FIN'd" signal) to
1080/// `H3_CLOSED_CRITICAL_STREAM`. Closure of the control stream or of either QPACK
1081/// side-channel is a connection error. Other I/O errors and any protocol error are passed
1082/// through unchanged.
1083fn map_critical_stream_eof(error: H3Error) -> H3Error {
1084 match error {
1085 H3Error::Io(e) if e.kind() == ErrorKind::UnexpectedEof => {
1086 H3ErrorCode::ClosedCriticalStream.into()
1087 }
1088 other => other,
1089 }
1090}
1091
1092async fn write(
1093 buf: &mut Vec<u8>,
1094 mut stream: impl AsyncWrite + Unpin + Send,
1095 mut f: impl FnMut(&mut [u8]) -> Option<usize>,
1096) -> io::Result<usize> {
1097 let written = loop {
1098 if let Some(w) = f(buf) {
1099 break w;
1100 }
1101 if buf.len() >= MAX_BUFFER_SIZE {
1102 return Err(io::Error::new(ErrorKind::OutOfMemory, "runaway allocation"));
1103 }
1104 buf.resize(buf.len() * 2, 0);
1105 };
1106
1107 stream.write_all(&buf[..written]).await?;
1108 stream.flush().await?;
1109 Ok(written)
1110}
1111
1112/// An `AsyncRead` adapter that drains a byte slice before reading from an inner stream.
1113///
1114/// Used to replay bytes that were read ahead while parsing a stream-type varint, before
1115/// dispatching to the inner runner that consumes the rest of the stream.
1116struct Prepended<'a, T> {
1117 head: &'a [u8],
1118 tail: T,
1119}
1120
1121impl<T: AsyncRead + Unpin> AsyncRead for Prepended<'_, T> {
1122 fn poll_read(
1123 self: Pin<&mut Self>,
1124 cx: &mut Context<'_>,
1125 out: &mut [u8],
1126 ) -> Poll<io::Result<usize>> {
1127 let this = self.get_mut();
1128 if !this.head.is_empty() {
1129 let n = this.head.len().min(out.len());
1130 out[..n].copy_from_slice(&this.head[..n]);
1131 this.head = &this.head[n..];
1132 return Poll::Ready(Ok(n));
1133 }
1134 Pin::new(&mut this.tail).poll_read(cx, out)
1135 }
1136}
1137
1138/// Read from `stream` into `buf` until `f` can decode a value.
1139///
1140/// `f` receives the filled portion of the buffer and returns:
1141/// - `Ok(Some((value, consumed)))` — success; consumed bytes are removed from the front
1142/// - `Ok(None)` — need more data; reads more bytes and retries
1143/// - `Err(e)` — unrecoverable error; propagated to caller
1144async fn read<R>(
1145 buf: &mut Vec<u8>,
1146 filled: &mut usize,
1147 stream: &mut (impl AsyncRead + Unpin + Send),
1148 f: impl Fn(&[u8]) -> Result<Option<(R, usize)>, H3ErrorCode>,
1149) -> Result<R, H3Error> {
1150 loop {
1151 if let Some((result, consumed)) = f(&buf[..*filled])? {
1152 buf.copy_within(consumed..*filled, 0);
1153 *filled -= consumed;
1154 return Ok(result);
1155 }
1156
1157 if *filled >= buf.len() {
1158 if buf.len() >= MAX_BUFFER_SIZE {
1159 return Err(io::Error::new(ErrorKind::OutOfMemory, "runaway allocation").into());
1160 }
1161 buf.resize(buf.len() * 2, 0);
1162 }
1163
1164 let n = stream.read(&mut buf[*filled..]).await?;
1165 if n == 0 {
1166 return Err(io::Error::new(ErrorKind::UnexpectedEof, "stream closed").into());
1167 }
1168 *filled += n;
1169 }
1170}
1171
1172#[cfg(test)]
1173mod tests;