churust_core/http3.rs
1//! HTTP/3 over QUIC (feature `http3`).
2//!
3//! HTTP/3 is a different transport, not a different framing of the same one:
4//! it runs over QUIC on UDP, so it needs its own socket and its own listener
5//! rather than an extra branch inside the TCP engine. That is why this is
6//! started separately from [`App::start`](crate::App::start), and why an
7//! application that wants both runs both.
8//!
9//! ```no_run
10//! use churust_core::{Call, Churust};
11//!
12//! # async fn run() -> std::io::Result<()> {
13//! let app = Churust::server()
14//! // Tell HTTP/1.1 and HTTP/2 clients that h3 is available on the same
15//! // port, so they can upgrade themselves on the next request.
16//! .advertise_http3(8443)
17//! .routing(|r| {
18//! r.get("/", |_c: Call| async { "served over h3" });
19//! })
20//! .build();
21//!
22//! churust_core::http3::serve(
23//! app,
24//! "0.0.0.0:8443".parse().unwrap(),
25//! "cert.pem",
26//! "key.pem",
27//! )
28//! .await
29//! # }
30//! ```
31//!
32//! # How clients find it
33//!
34//! They do not, on their own. A browser reaches a new origin over TCP, and only
35//! moves to h3 if the response says h3 exists. That is the `Alt-Svc` header, and
36//! [`AppBuilder::advertise_http3`](crate::AppBuilder::advertise_http3) is what
37//! sets it. Serving h3 without advertising it means almost nothing will ever
38//! use it.
39//!
40//! # What is supported
41//!
42//! Request routing, headers, request bodies and response bodies, including
43//! streamed ones, through the same pipeline every other transport uses: a
44//! handler cannot tell which transport it is answering.
45//!
46//! WebSockets are not carried over h3. The upgrade mechanism there is Extended
47//! CONNECT from RFC 9220, which is a different handshake from the HTTP/1.1 one
48//! the `ws` feature implements, and pretending otherwise would produce a
49//! connection that fails at the first frame.
50
51#![cfg(feature = "http3")]
52
53use crate::app::App;
54use crate::body::Body;
55use crate::pem::{load_certs, load_key};
56use bytes::{Buf, Bytes};
57use http::{HeaderMap, Method, Request, Uri};
58use std::io;
59use std::net::SocketAddr;
60use std::sync::Arc;
61
62/// How long a QUIC connection may sit idle before quinn closes it, when no
63/// value is supplied.
64///
65/// Matches the default `keep_alive_ms`, so an idle connection costs the same
66/// whichever transport it arrived on. [`serve`] passes the app's own
67/// `keep_alive_ms` instead; this is what [`server_config_from_pem`] uses, since
68/// it is a free function with no access to the app config.
69const DEFAULT_IDLE_MS: u64 = 75_000;
70
71/// The per-connection request-stream cap used when `h2_max_concurrent_streams`
72/// is `0`.
73///
74/// Matches the default `h2_max_concurrent_streams`, for the reason spelled out on
75/// [`max_streams_for`]: HTTP/2's "no limit" has no safe QUIC translation, because
76/// quinn allocates eagerly against whatever is advertised.
77const DEFAULT_MAX_STREAMS: u32 = 200;
78
79/// Build a QUIC server configuration from a PEM certificate chain and key.
80///
81/// The ALPN protocol is `h3` and nothing else: a QUIC connection that
82/// negotiated anything else would not be HTTP/3, and there is nothing here to
83/// hand it to.
84///
85/// The transport bounds are the defaults, because there is no app here to ask.
86/// [`serve`] builds its config through the same code with the app's own
87/// settings, so the knobs are honoured on the ordinary path; a caller reaching
88/// for this function directly and wanting other values adjusts the returned
89/// config's transport before handing it to [`serve_with_config`].
90///
91/// # Errors
92///
93/// If either file is missing or unreadable, if no private key is found, or if
94/// rustls rejects the pair.
95pub fn server_config_from_pem(cert_path: &str, key_path: &str) -> io::Result<quinn::ServerConfig> {
96 server_config_from_pem_with_limits(cert_path, key_path, TransportLimits::defaults())
97}
98
99/// The QUIC idle bound for a configured `keep_alive_ms`.
100///
101/// `0` means "answer and close" on TCP. QUIC has no equivalent: a connection
102/// multiplexes streams, and `max_idle_timeout(None)` is the opposite — it never
103/// expires, which would unbound the `max_connections` slot the timeout exists to
104/// release. Fall back to the default bound rather than invent a QUIC meaning for
105/// a TCP-shaped setting.
106fn idle_ms_for(keep_alive_ms: u64) -> u64 {
107 match keep_alive_ms {
108 0 => DEFAULT_IDLE_MS,
109 ms => ms,
110 }
111}
112
113/// Convert milliseconds into a QUIC idle timeout.
114///
115/// An error rather than an `expect`, because the value is no longer a constant:
116/// a QUIC idle timeout is carried as a varint and rejects anything past its
117/// range, and `keep_alive_ms` is a `u64` an operator fills in. Refusing at
118/// startup names the bad setting; panicking would take the process down for it.
119fn idle_timeout(idle_ms: u64) -> io::Result<quinn::IdleTimeout> {
120 std::time::Duration::from_millis(idle_ms)
121 .try_into()
122 .map_err(|e| {
123 io::Error::new(
124 io::ErrorKind::InvalidInput,
125 format!("keep_alive_ms = {idle_ms} is too large for a QUIC idle timeout: {e}"),
126 )
127 })
128}
129
130/// The per-connection stream cap for a configured `h2_max_concurrent_streams`.
131///
132/// A cap matters more here than the name suggests. Every in-flight request
133/// buffers its body up to `max_body_bytes`, so what a single connection can make
134/// this process hold is that cap multiplied by the streams it may open at once —
135/// and quinn's own default of 100 applied however `h2_max_concurrent_streams`
136/// was set, which is the same knob-ignored-on-one-transport shape as the idle
137/// timeout above.
138///
139/// # Why `0` is not passed through
140///
141/// `0` removes the limit on HTTP/2, and the QUIC transport parameter has a
142/// maximum that looks like the natural translation. It is not: quinn *allocates
143/// against this number eagerly*. `quinn_proto::connection::streams::state::State`
144/// pre-inserts one map entry per advertised remote stream when the connection
145/// state is built — `for i in 0..max_remote[dir] { insert(...) }` — so a varint
146/// maximum of `2^62 - 1` is a four-quintillion-iteration loop that runs on the
147/// first Initial packet from any peer, before the handshake completes and before
148/// anything is authenticated. Advertising it does not fail cleanly at the far
149/// end; it pins a core and grows memory locally, on one unauthenticated UDP
150/// packet. Measured: a listener built this way never completed a handshake.
151///
152/// That rules out "large but not maximal" too — the loop is linear in whatever is
153/// advertised, so `1 << 60` is the same hang with a different constant.
154///
155/// So `0` falls back to the default, the same shape [`idle_ms_for`] uses for a
156/// `keep_alive_ms` of `0`: a setting whose HTTP/2 meaning has no safe QUIC
157/// translation keeps the documented default rather than being invented.
158fn max_streams_for(h2_max_concurrent_streams: u32) -> quinn::VarInt {
159 match h2_max_concurrent_streams {
160 0 => quinn::VarInt::from_u32(DEFAULT_MAX_STREAMS),
161 n => quinn::VarInt::from_u32(n),
162 }
163}
164
165/// The QUIC transport bounds that come from the app's own configuration.
166///
167/// Grouped so there is one list of them: a *transport parameter* that belongs
168/// here and is not in this struct is one HTTP/3 silently ignores, which is the
169/// failure this type exists to make visible.
170///
171/// Only quinn transport parameters, because that is all this reaches — it is
172/// handed to [`server_config_from_pem_with_limits`] and nothing else. App-level
173/// deadlines are applied where the work they bound happens, in
174/// [`serve_connection`]: `h2_max_header_list_size` becomes h3's
175/// `max_field_section_size` there, and consequently applies on the
176/// [`serve_with_config`] path too, where these bounds do not.
177struct TransportLimits {
178 /// From `keep_alive_ms`, via [`idle_ms_for`].
179 idle_ms: u64,
180 /// From `h2_max_concurrent_streams`, via [`max_streams_for`].
181 max_streams: quinn::VarInt,
182}
183
184impl TransportLimits {
185 /// The bounds this app configures.
186 fn from(cfg: &crate::app::ServerConfig) -> Self {
187 Self {
188 idle_ms: idle_ms_for(cfg.keep_alive_ms),
189 max_streams: max_streams_for(cfg.h2_max_concurrent_streams),
190 }
191 }
192
193 /// The defaults, for a caller with no app to ask.
194 fn defaults() -> Self {
195 Self::from(&crate::app::ServerConfig::default())
196 }
197}
198
199/// [`server_config_from_pem`], with the transport bounds named rather than
200/// assumed.
201///
202/// The one place QUIC transport bounds are applied, so a value cannot drift away
203/// from the setting it came from the way a second copy would.
204fn server_config_from_pem_with_limits(
205 cert_path: &str,
206 key_path: &str,
207 limits: TransportLimits,
208) -> io::Result<quinn::ServerConfig> {
209 let certs = load_certs(cert_path)?;
210 let key = load_key(key_path)?;
211
212 let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
213 .with_no_client_auth()
214 .with_single_cert(certs, key)
215 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
216 tls.alpn_protocols = vec![b"h3".to_vec()];
217
218 let quic = quinn::crypto::rustls::QuicServerConfig::try_from(tls)
219 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
220 let mut config = quinn::ServerConfig::with_crypto(Arc::new(quic));
221
222 // Bound the connection's life, not just the request's. A permit is held for
223 // as long as the QUIC connection lives, and quinn's default idle timeout is
224 // negotiated with the peer — so a client that opens a connection and then
225 // says nothing at all, or answers one request and lingers, pinned a
226 // `max_connections` slot indefinitely. Neither case involves a request, so
227 // no request-level deadline can reach them.
228 let mut transport = quinn::TransportConfig::default();
229 transport.max_idle_timeout(Some(idle_timeout(limits.idle_ms)?));
230 // Bidirectional only: an HTTP/3 request arrives on a bidi stream, and the
231 // unidirectional ones carry control and QPACK data rather than requests, so
232 // a request-concurrency setting is not the right bound for them.
233 transport.max_concurrent_bidi_streams(limits.max_streams);
234 config.transport_config(Arc::new(transport));
235 Ok(config)
236}
237
238/// Serve `app` over HTTP/3 on `addr` until the process ends.
239///
240/// The transport bounds come from the app: `keep_alive_ms` becomes the QUIC idle
241/// timeout and `h2_max_concurrent_streams` the per-connection request limit, so
242/// lowering either bounds this transport too and not only TCP.
243///
244/// `h2_max_header_list_size` is honoured as well, applied per stream rather than
245/// as a QUIC transport parameter, so unlike the two above it holds on
246/// [`serve_with_config`] too.
247///
248/// `header_read_timeout_ms` is *not* applied on this transport. See the note in
249/// `serve_connection` for why — abandoning a pending request in h3 0.4 leaks its
250/// stream id — and for what bounds the exposure instead.
251///
252/// # Errors
253///
254/// If the certificate or key cannot be loaded, if no private key is found, if
255/// rustls rejects the pair, if `keep_alive_ms` is too large for a QUIC idle
256/// timeout, or if the UDP socket cannot be bound.
257pub async fn serve(app: App, addr: SocketAddr, cert_path: &str, key_path: &str) -> io::Result<()> {
258 let limits = TransportLimits::from(app.config());
259 let config = server_config_from_pem_with_limits(cert_path, key_path, limits)?;
260 serve_with_config(app, addr, config).await
261}
262
263/// Serve `app` over HTTP/3 with an already-built QUIC configuration.
264///
265/// The configuration is used as given: `keep_alive_ms` is not applied here,
266/// because a caller who built the transport themselves has already said what
267/// they want. [`serve`] is the path that reads the app's knob.
268///
269/// # Errors
270///
271/// If the UDP socket cannot be bound.
272pub async fn serve_with_config(
273 app: App,
274 addr: SocketAddr,
275 config: quinn::ServerConfig,
276) -> io::Result<()> {
277 Http3Server::bind(addr, config)?.serve(app).await;
278 Ok(())
279}
280
281/// A bound QUIC listener, not yet serving.
282///
283/// Binding and serving are separate steps so the port can be read back before
284/// anything is accepted. That matters for the ordinary case of binding port 0
285/// and telling something else where to connect, and it is what lets a test know
286/// the address without racing the server.
287pub struct Http3Server {
288 endpoint: quinn::Endpoint,
289}
290
291impl std::fmt::Debug for Http3Server {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 f.debug_struct("Http3Server")
294 .field("local_addr", &self.endpoint.local_addr().ok())
295 .finish_non_exhaustive()
296 }
297}
298
299impl Http3Server {
300 /// Bind a UDP socket for QUIC without serving yet.
301 ///
302 /// # Errors
303 ///
304 /// If the socket cannot be bound.
305 pub fn bind(addr: SocketAddr, config: quinn::ServerConfig) -> io::Result<Self> {
306 Ok(Self {
307 endpoint: quinn::Endpoint::server(config, addr)?,
308 })
309 }
310
311 /// The address actually bound, which is what resolves a port of 0.
312 ///
313 /// # Errors
314 ///
315 /// If the socket cannot report its address.
316 pub fn local_addr(&self) -> io::Result<SocketAddr> {
317 self.endpoint.local_addr()
318 }
319
320 /// Accept connections and serve them through `app` until the endpoint
321 /// closes.
322 pub async fn serve(self, app: App) {
323 accept_loop(app, self.endpoint).await;
324 }
325}
326
327/// Accept QUIC connections until the endpoint closes.
328async fn accept_loop(app: App, endpoint: quinn::Endpoint) {
329 // The same connection budget the TCP engine applies. Without it, enabling
330 // http3 opted a deployment out of `max_connections` entirely — and
331 // `advertise_http3` actively steers clients here.
332 let slots = (app.config().max_connections > 0)
333 .then(|| std::sync::Arc::new(tokio::sync::Semaphore::new(app.config().max_connections)));
334
335 // `max_tls_handshakes` and `tls_handshake_timeout_ms`, applied to QUIC for
336 // the reason they exist on TCP: a QUIC handshake *is* a TLS 1.3 handshake,
337 // asymmetric in exactly the same way — cheap to ask for, expensive to
338 // answer — so `max_connections` alone is too loose a bound on it. Enabling
339 // http3 previously opted a deployment out of both knobs, which is the same
340 // hole `slots` above was added to close.
341 //
342 // The existing knobs rather than QUIC-specific ones, on the precedent
343 // `serve_connection` sets just below for `h2_max_header_list_size`: the
344 // setting asks the same question of the same kind of work, so one value
345 // should not need saying twice.
346 let handshakes = (app.config().max_tls_handshakes > 0)
347 .then(|| std::sync::Arc::new(tokio::sync::Semaphore::new(app.config().max_tls_handshakes)));
348 let handshake_timeout = (app.config().tls_handshake_timeout_ms > 0)
349 .then(|| std::time::Duration::from_millis(app.config().tls_handshake_timeout_ms));
350
351 while let Some(incoming) = endpoint.accept().await {
352 let app = app.clone();
353 // Taken before accepting, so excess load waits in QUIC's own queue
354 // rather than as memory in this process. Held for the connection's
355 // life by the task below.
356 let permit = match &slots {
357 Some(sem) => sem.clone().acquire_owned().await.ok(),
358 None => None,
359 };
360 let handshakes = handshakes.clone();
361 // One task per connection, like the TCP engine: a slow peer must not
362 // hold up the accept loop.
363 tokio::spawn(async move {
364 let _permit = permit;
365
366 // Queueing for the handshake budget and performing the handshake
367 // are one timed step, as on TCP. Timing only the handshake would
368 // turn the budget into a rate limiter: permits would expire at
369 // `max_tls_handshakes` per deadline while everything else waited on
370 // `acquire_owned` with no deadline at all, and each of those waiters
371 // is already holding a connection permit taken above. What the knob
372 // promises is that no peer holds a connection permit for longer
373 // than this without having proved it can speak TLS, so the wait for
374 // the budget has to be inside the deadline that guards it.
375 //
376 // The handshake permit is scoped to this block and released when it
377 // ends — before `serve_connection` runs. The handshake budget is
378 // for handshakes; the connection budget takes over from there.
379 let queue_and_shake = async {
380 let _handshake_permit = match &handshakes {
381 Some(sem) => sem.clone().acquire_owned().await.ok(),
382 None => None,
383 };
384 incoming.await
385 };
386
387 let accepted = match handshake_timeout {
388 Some(limit) => match tokio::time::timeout(limit, queue_and_shake).await {
389 Ok(res) => res,
390 // Dropping the future cancels the acquire as well as the
391 // handshake, so a peer that timed out while queued returns
392 // nothing and holds nothing. Until the handshake completes
393 // there is no HTTP layer, so no request-level deadline can
394 // reach this.
395 Err(_) => {
396 tracing::debug!("http3 handshake timed out");
397 return;
398 }
399 },
400 None => queue_and_shake.await,
401 };
402
403 match accepted {
404 Ok(connection) => {
405 if let Err(e) = serve_connection(app, connection).await {
406 tracing::debug!(error = %e, "http3 connection ended");
407 }
408 }
409 // A failed handshake on an internet-facing port is constant
410 // background noise, so debug rather than warn, matching how the
411 // TCP engine treats a failed TLS handshake.
412 Err(e) => tracing::debug!(error = %e, "http3 handshake failed"),
413 }
414 });
415 }
416}
417
418/// Run one QUIC connection's request streams through the pipeline.
419async fn serve_connection(
420 app: App,
421 connection: quinn::Connection,
422) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
423 // The peer address, which the TCP engine seeds and this path did not — so
424 // `Call::peer_addr` was `None` and anything keying on it (rate limiting,
425 // audit logs, IP allowlists) treated the entire h3 fleet as one client.
426 let peer = connection.remote_address();
427
428 // h3's default `max_field_section_size` is `VarInt::MAX`, i.e. no bound on
429 // a header block at all, where the TCP engine caps HTTP/1 headers by count
430 // and HTTP/2 by encoded size. Reuse the HTTP/2 knob: it asks the same
431 // question of the same kind of header block.
432 let mut h3 = h3::server::builder()
433 .max_field_section_size(app.config().h2_max_header_list_size as u64)
434 .build(h3_quinn::Connection::new(connection))
435 .await?;
436
437 // `header_read_timeout_ms` is deliberately NOT applied to the wait for a
438 // HEADERS frame here, and this is the note saying why, because the omission
439 // looks exactly like the transport-parity bugs fixed elsewhere in this file.
440 //
441 // It was applied, by wrapping `resolve_request` in a timeout and returning
442 // when it elapsed. That leaks. h3 0.0.8 inserts the stream id into
443 // `h3::server::Connection::ongoing_streams` in `poll_accept_bi`, as soon as
444 // the bidi stream is accepted and before any frame is read, and removes it
445 // only when `RequestEnd::drop` reports the id back. `RequestResolver` holds a
446 // bare `UnboundedSender<StreamId>` rather than a `RequestEnd` — the
447 // `RequestEnd` is built inside `accept_with_frame`, i.e. *after* the header
448 // block has decoded — so a resolver abandoned before that point can never
449 // report anything. The id then stays in the set for the life of the
450 // connection: `ongoing_streams.is_empty()` is never true again, the
451 // connection can never complete its graceful close, and the task keeps its
452 // `max_connections` permit. A peer repeating the stall grows that set without
453 // bound. That is the permit leak the deadline was meant to prevent, made
454 // permanent instead of temporary.
455 //
456 // There is no way to abandon a pending resolver cleanly in this version, so
457 // the wait is left unbounded and the exposure is bounded structurally
458 // instead: `h2_max_concurrent_streams` caps the streams one connection may
459 // have open at once, and `max_connections` caps the connections. Closing the
460 // whole QUIC connection on the deadline would also avoid the leak, but takes
461 // every sibling request down with it, which the accept loop's own error
462 // handling deliberately does not do.
463 //
464 // If h3 gains a way to end a pending request — a `RequestEnd` on the
465 // resolver, or a documented reset that reports the id — this becomes a small
466 // change and should be made.
467
468 loop {
469 match h3.accept().await {
470 Ok(Some(resolver)) => {
471 let app = app.clone();
472 // One task per request: h3 multiplexes streams on one
473 // connection, so serving them in sequence would make a slow
474 // handler block every other request on that connection.
475 //
476 // Resolving belongs inside the task for the same reason, and
477 // for a second one. `accept` returns as soon as a bidi stream
478 // exists, before any frame has been read, so awaiting
479 // `resolve_request` out here waited for one peer's HEADERS
480 // frame before the next stream could even be accepted — a
481 // stream whose headers never arrive head-of-line blocked every
482 // request behind it. And its error is a *stream* error:
483 // propagating it with `?` returned from this function, dropped
484 // the `h3::server::Connection`, and closed the whole QUIC
485 // connection, killing every other request multiplexed on it.
486 tokio::spawn(async move {
487 let (request, stream) = match resolver.resolve_request().await {
488 Ok(pair) => pair,
489 // Scoped to this stream by construction. h3 has already
490 // done whatever the stream needed — a `431` for an
491 // oversized header block, a reset for one that ended
492 // before its headers — and the connection is untouched.
493 //
494 // The genuinely connection-fatal case is not lost by
495 // handling it here: h3 records it on the shared
496 // connection state, so the next `accept` above returns
497 // it and the `Err` arm ends the connection then. That
498 // path is also *better* than the `?` was, because it
499 // lets h3 emit the real code — H3_FRAME_UNEXPECTED,
500 // say — where dropping the connection sent
501 // H3_NO_ERROR for what was actually a protocol
502 // violation.
503 Err(e) => {
504 tracing::debug!(error = %e, "http3 request headers failed");
505 return;
506 }
507 };
508 if let Err(e) = serve_request(app, request, stream, peer).await {
509 tracing::debug!(error = %e, "http3 request failed");
510 }
511 });
512 }
513 // The peer closed the connection cleanly.
514 Ok(None) => return Ok(()),
515 Err(e) => return Err(e.into()),
516 }
517 }
518}
519
520/// Answer one HTTP/3 request.
521async fn serve_request<S>(
522 app: App,
523 request: Request<()>,
524 mut stream: h3::server::RequestStream<S, Bytes>,
525 peer: SocketAddr,
526) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
527where
528 S: h3::quic::BidiStream<Bytes>,
529{
530 let (parts, _) = request.into_parts();
531 let max_body = app.config().max_body_bytes;
532
533 // RFC 9114 §4.2: a message carrying a connection-specific field must be
534 // treated as malformed. Checked before the body is judged for size, because a
535 // malformed message is not a request whose length is worth an opinion.
536 //
537 // The reason this is a refusal and not a strip, when the response path
538 // strips: a `Transfer-Encoding` on an inbound h3 request is a framing claim
539 // on a transport that already frames the body itself, and quietly ignoring it
540 // is how the same bytes come to mean two different things to a proxy in front
541 // and this server behind. The TCP path refuses the HTTP/1.1 version of that
542 // disagreement for the same reason.
543 if let Some(field) = connection_specific_field(&parts.headers) {
544 tracing::debug!(
545 field,
546 "rejected an http3 request with a connection-specific field"
547 );
548 return send_status(&app, &mut stream, http::StatusCode::BAD_REQUEST).await;
549 }
550
551 // Refuse a body the client has already declared too large, before a byte of
552 // it is read — the same check the TCP path makes for the same reason.
553 // `read_body` below refuses at the chunk that crosses the cap, which bounds
554 // what is held but still accepts and buffers everything up to it.
555 // `Content-Length` is the client's own statement of size, so refusing on it
556 // costs one header lookup and no buffering at all. A body that declares
557 // nothing is still bounded by the cap when it is read.
558 let declared: Option<u64> = parts
559 .headers
560 .get(http::header::CONTENT_LENGTH)
561 .and_then(|v| v.to_str().ok())
562 .and_then(|v| v.parse::<u64>().ok());
563 if let Some(declared) = declared {
564 if declared > max_body as u64 {
565 return send_status(&app, &mut stream, http::StatusCode::PAYLOAD_TOO_LARGE).await;
566 }
567 }
568
569 // The deadline has to start here, not after the body has arrived. Reading
570 // the body is the attacker-controlled phase — a client can dribble one byte
571 // and stop — and wrapping only the handler left exactly that phase
572 // unbounded, which the TCP path does not do.
573 let timeout_ms = app.config().request_timeout_ms;
574 let deadline = (timeout_ms > 0)
575 .then(|| tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms));
576
577 let read = async { read_body(&mut stream, max_body, declared).await };
578 let read = match deadline {
579 Some(at) => match tokio::time::timeout_at(at, read).await {
580 Ok(r) => r,
581 Err(_) => {
582 return send_status(&app, &mut stream, http::StatusCode::REQUEST_TIMEOUT).await;
583 }
584 },
585 None => read.await,
586 };
587
588 let body = match read {
589 Ok(body) => body,
590 Err(BodyRefused::TooLarge) => {
591 return send_status(&app, &mut stream, http::StatusCode::PAYLOAD_TOO_LARGE).await;
592 }
593 // Reset rather than a 400, and the reason is what can actually reach
594 // the peer. The stream failed while we were reading it, so in the
595 // common case the peer has already reset its side — and a client that
596 // resets a request stream is cancelling it, which under RFC 9114 §4.1
597 // means it also stops reading the response. A status written into that
598 // is discarded, and when the failure was a `ConnectionError` there is
599 // no connection left to write it to at all. A 400 would therefore be a
600 // status this server believes it sent and the client never saw.
601 //
602 // Returning without resetting is not an option either, for the reason
603 // spelled out on the streamed-response arm below: the `RequestStream`
604 // is dropped on the way out and quinn finishes a stream when it drops,
605 // so the peer would get a clean FIN. Resetting first is what wins that
606 // race, and H3_REQUEST_INCOMPLETE is the code RFC 9114 defines for
607 // exactly this — "the client's stream terminated without containing a
608 // fully-formed request".
609 //
610 // The part that matters most is above this line rather than in it: we
611 // return before `process_with_extensions`, so the handler never runs
612 // and never commits half a payload. Refusing the stream is the report;
613 // not dispatching is the fix.
614 // A 400 rather than a stream error, and rather than the reset the arm
615 // below uses. The peer finished cleanly and is still reading its
616 // response, so the objection reaches it — which is the very thing the
617 // reset arm says is not true of a cancelled stream. It also matches what
618 // HTTP/1.1 does with the same request. Not followed by `stop_stream`:
619 // `send_status` finishes the stream, and a reset after it can destroy the
620 // response before it is acknowledged.
621 Err(BodyRefused::Truncated { declared, got }) => {
622 tracing::debug!(
623 declared,
624 got,
625 "http3 request body did not match its content-length"
626 );
627 return send_status(&app, &mut stream, http::StatusCode::BAD_REQUEST).await;
628 }
629 Err(BodyRefused::Incomplete(e)) => {
630 tracing::debug!(error = %e, "http3 request body ended before it was complete");
631 stream.stop_stream(h3::error::Code::H3_REQUEST_INCOMPLETE);
632 return Ok(());
633 }
634 };
635
636 let mut extensions = http::Extensions::new();
637 extensions.insert(crate::call::PeerAddr(peer));
638
639 // Carry the authority across in the `Host` field, because `normalise_uri`
640 // below is about to drop it.
641 //
642 // `Call::host` reads the URI authority first and the `Host` field only as a
643 // fallback (`call.rs`), and HTTP/3 replaced the field with the `:authority`
644 // pseudo-header, which arrives in the URI. Normalising to origin form
645 // therefore removed the only signal there was: `Call::host` answered `None`
646 // over h3, so `guard::host` matched nothing and a host-scoped route was
647 // either a silent 404 or — with an unguarded sibling on the same method and
648 // path, which is the ordinary virtual-host shape — served by the wrong
649 // handler. `guard.rs` documents this same failure as a fixed HTTP/2
650 // regression; on this transport it had come back.
651 //
652 // Seeding the field rather than keeping the authority in the URI is what
653 // leaves `normalise_uri`'s own decision intact: a handler still sees the
654 // same origin-form target on every transport.
655 //
656 // `insert`, not `append`: `Call::host` resolves a request carrying both an
657 // authority and a `Host` field to the authority, so a stray field must not
658 // survive beside the value it is supposed to lose to.
659 let mut headers = parts.headers.clone();
660 if let Some(authority) = parts.uri.authority() {
661 if let Ok(value) = http::HeaderValue::from_str(authority.as_str()) {
662 headers.insert(http::header::HOST, value);
663 }
664 }
665
666 let process = app.process_with_extensions(
667 parts.method.clone(),
668 normalise_uri(&parts.uri, &parts.method),
669 headers,
670 body,
671 extensions,
672 );
673
674 // The remainder of the same budget, so the whole exchange is bounded once
675 // rather than the body and the handler each getting a full allowance.
676 let response = match deadline {
677 None => process.await,
678 Some(at) => match tokio::time::timeout_at(at, process).await {
679 Ok(res) => res,
680 Err(_) => crate::response::Response::text("Request Timeout")
681 .with_status(http::StatusCode::REQUEST_TIMEOUT),
682 },
683 };
684
685 send_response(&app, &mut stream, response, parts.method == Method::HEAD).await
686}
687
688/// Answer with a bare status and nothing else, hardened like any other reply.
689///
690/// The refusals above are composed here rather than in the pipeline, which is
691/// why they need their own call to `apply_security_headers`: the middleware
692/// that would otherwise add them never runs for a request that was never
693/// dispatched. Funnelled through one function so the next refusal added to
694/// `serve_request` cannot forget.
695async fn send_status<S>(
696 app: &App,
697 stream: &mut h3::server::RequestStream<S, Bytes>,
698 status: http::StatusCode,
699) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
700where
701 S: h3::quic::BidiStream<Bytes>,
702{
703 let mut response = http::Response::builder().status(status).body(())?;
704 app.apply_security_headers(response.headers_mut(), true);
705 stream.send_response(response).await?;
706 stream.finish().await?;
707 Ok(())
708}
709
710/// Why a request body was not handed to the pipeline.
711enum BodyRefused {
712 /// The request body exceeded the server's cap.
713 TooLarge,
714 /// The stream failed before the body was whole, so what was read is a
715 /// fragment of a request rather than a request.
716 Incomplete(h3::error::StreamError),
717 /// The body ended cleanly at a length the client's own `Content-Length` says
718 /// it should not have.
719 ///
720 /// Distinct from `Incomplete`: the peer finished its side deliberately and is
721 /// still reading, so unlike a reset this one can be told what was wrong.
722 Truncated { declared: u64, got: u64 },
723}
724
725/// Read a request body, refusing one past `max_body` and one cut short.
726///
727/// Counted as it arrives rather than collected and then measured, so an
728/// oversized body is refused at the chunk that crosses the line instead of
729/// after all of it has been held in memory.
730///
731/// # Why this buffers where the TCP engine streams
732///
733/// Deliberately, and not because streaming was overlooked. Collecting the body
734/// before dispatch is what lets a truncated one be refused *without the handler
735/// ever running*, which is the guarantee the `Incomplete` arm below exists for:
736/// a peer that announces 5000 bytes, sends 1200 and resets has sent a fragment,
737/// and a fragment is undetectable after the fact because a truncated body is a
738/// well-formed shorter body.
739///
740/// The TCP path hands the handler a stream instead, so there the truncation
741/// surfaces mid-handler — after any side effect it has on the bytes it already
742/// read. `Call::body_stream` says as much: exceeding the cap "surfaces as an
743/// error item in the stream rather than a `413`, because the response has
744/// usually begun by then". This transport is the stricter of the two, and the
745/// cost is memory: one `max_body_bytes` per in-flight request.
746///
747/// That cost is bounded rather than removed. `TransportLimits::max_streams`
748/// caps the requests a connection may have in flight, so the ceiling is
749/// `max_body_bytes × h2_max_concurrent_streams × max_connections` and every
750/// term is configurable; a declared oversize is refused in `serve_request`
751/// before a byte is buffered.
752///
753/// Converting this to a stream would level the two transports down to the
754/// weaker guarantee. If the asymmetry is worth closing, close it the other way.
755async fn read_body<S>(
756 stream: &mut h3::server::RequestStream<S, Bytes>,
757 max_body: usize,
758 declared: Option<u64>,
759) -> Result<Bytes, BodyRefused>
760where
761 S: h3::quic::BidiStream<Bytes>,
762{
763 let mut buf = bytes::BytesMut::new();
764 // `recv_data` has three outcomes and they have to stay three. This was a
765 // `while let Ok(Some(..))`, which is only two: a chunk, or the loop ends.
766 // The clean end of body and a stream that failed both fell into "the loop
767 // ends", so a body cut short — a peer that announced 5000 bytes, sent
768 // 1200, then RESET_STREAM, which surfaces here as
769 // `StreamError::RemoteTerminate` — was returned as `Ok` and dispatched as
770 // if the whole request had arrived. The handler ran, committed whatever
771 // side effect it has on a fragment of the payload, and answered 200. That
772 // is the failure mode the caller cannot detect afterwards, because a
773 // truncated body is a well-formed shorter body.
774 loop {
775 match stream.recv_data().await {
776 Ok(Some(mut chunk)) => {
777 let piece = chunk.copy_to_bytes(chunk.remaining());
778 if buf.len() + piece.len() > max_body {
779 return Err(BodyRefused::TooLarge);
780 }
781 buf.extend_from_slice(&piece);
782 }
783 // The peer finished its side of the stream. That makes the body
784 // *complete*, which is not the same as whole: a clean FIN after 1200
785 // bytes of a declared 5000 is exactly the fragment the `Incomplete`
786 // arm below exists to refuse, arriving by the one route that used to
787 // be taken as proof of wholeness. Measured, this transport served it
788 // as a 200 where HTTP/1.1 answered 400.
789 //
790 // `!=` rather than `<`, so a body longer than its declaration is
791 // caught too — that disagreement is the request-smuggling shape the
792 // TCP path refuses outright, and there is no reading of it that makes
793 // one of the two numbers right.
794 Ok(None) => {
795 let got = buf.len() as u64;
796 return match declared {
797 Some(n) if n != got => Err(BodyRefused::Truncated { declared: n, got }),
798 _ => Ok(buf.freeze()),
799 };
800 }
801 Err(e) => return Err(BodyRefused::Incomplete(e)),
802 }
803 }
804}
805
806/// HTTP/3 always carries an absolute-form target, TCP requests usually do not.
807///
808/// The router matches on the path, and `Call::path` reads it off the URI, so
809/// both forms already work. What differs is what a handler sees when it prints
810/// the URI, and an absolute form there would be a gratuitous difference between
811/// transports. `CONNECT` and `OPTIONS *` are left alone: their target is not a
812/// path and rewriting it would destroy the request.
813fn normalise_uri(uri: &Uri, method: &Method) -> Uri {
814 if method == Method::CONNECT || uri.path() == "*" {
815 return uri.clone();
816 }
817 let Some(path_and_query) = uri.path_and_query() else {
818 return uri.clone();
819 };
820 path_and_query
821 .as_str()
822 .parse()
823 .unwrap_or_else(|_| uri.clone())
824}
825
826/// Write a [`Response`](crate::Response) out over an h3 stream.
827async fn send_response<S>(
828 app: &App,
829 stream: &mut h3::server::RequestStream<S, Bytes>,
830 response: crate::Response,
831 head_only: bool,
832) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
833where
834 S: h3::quic::BidiStream<Bytes>,
835{
836 let mut builder = http::Response::builder().status(response.status);
837 if let Some(headers) = builder.headers_mut() {
838 *headers = sanitise(response.headers);
839 // `true`, which only this module is in a position to assert.
840 // QUIC has no plaintext mode — `server_config_from_pem` pins TLS 1.3,
841 // and a connection that negotiated anything but `h3` never gets here —
842 // so a response written to this stream is encrypted whatever
843 // `AppBuilder::tls` was told. The pipeline cannot know that: it runs
844 // the same code on every transport and reads a builder field that
845 // `http3::serve` has no way to set, since it is handed the certificate
846 // as an argument and the `App` is already built. The result was that
847 // the one transport where TLS is mandatory was the one that never sent
848 // HSTS.
849 //
850 // Widening the gate, not bypassing what is behind it: `hsts(None)` and
851 // `without_security_headers` are still obeyed, and a handler that set
852 // the header keeps its own value.
853 //
854 // Most of what this adds is already there — the pipeline's own
855 // middleware ran for anything routed — but a response the pipeline
856 // never produced (the `408` from an expired deadline just above)
857 // arrives with nothing, and that is the second reason this is here
858 // rather than in `serve_request`'s happy path.
859 app.apply_security_headers(headers, true);
860 }
861 stream.send_response(builder.body(())?).await?;
862
863 if !head_only {
864 match response.body {
865 Body::Bytes(bytes) => {
866 if !bytes.is_empty() {
867 stream.send_data(bytes).await?;
868 }
869 }
870 Body::Stream(mut chunks) => {
871 use futures_util::StreamExt;
872 // Forwarded chunk by chunk, so a streamed response stays
873 // streamed over h3 instead of being collected to send it.
874 while let Some(chunk) = chunks.next().await {
875 match chunk {
876 Ok(bytes) => stream.send_data(bytes).await?,
877 // The body failed partway. There is no status left to
878 // change, so the honest signal is an incomplete
879 // stream: reset it rather than finishing cleanly and
880 // claiming the truncated body was the whole thing.
881 //
882 // Returning without resetting did the opposite of what
883 // this comment promised. It skips the `finish` below,
884 // but the `RequestStream` is owned by this task and is
885 // dropped on the way out — and quinn's `SendStream`
886 // finishes the stream when it drops. The peer got a
887 // clean FIN and a truncated body it could not tell
888 // apart from the whole one: three records of a hundred,
889 // stored as the complete answer. Over HTTP/1.1 and
890 // HTTP/2 the same handler surfaces the error to hyper,
891 // which aborts.
892 //
893 // Resetting first is what wins the race: after
894 // `stop_stream` the drop-time finish fails with
895 // `ClosedStream`, which quinn ignores, so RESET_STREAM
896 // is what reaches the peer.
897 Err(e) => {
898 tracing::debug!(error = %e, "http3 response body failed mid-stream");
899 stream.stop_stream(h3::error::Code::H3_INTERNAL_ERROR);
900 return Ok(());
901 }
902 }
903 }
904 }
905 }
906 }
907
908 stream.finish().await?;
909 Ok(())
910}
911
912/// The fields RFC 9114 §4.2 bans from an HTTP/3 message.
913///
914/// One list because the rule is symmetric and the two directions are not: a
915/// response carrying one of these is stripped, because the handler that set it
916/// was probably written for HTTP/1.1 and the alternative is a client rejecting a
917/// reply the application meant; a *request* carrying one is refused, because the
918/// spec says an endpoint must treat such a message as malformed and the sender is
919/// the one that can fix it. Two lists would eventually disagree about which
920/// fields those are.
921const CONNECTION_SPECIFIC: [&str; 5] = [
922 "connection",
923 "keep-alive",
924 "proxy-connection",
925 "transfer-encoding",
926 "upgrade",
927];
928
929/// Drop headers HTTP/3 forbids.
930///
931/// RFC 9114 §4.2 bans connection-specific fields, and a `Connection:
932/// keep-alive` copied from a handler written for HTTP/1.1 is enough for a
933/// conforming client to treat the whole response as malformed.
934fn sanitise(mut headers: HeaderMap) -> HeaderMap {
935 for name in CONNECTION_SPECIFIC {
936 headers.remove(name);
937 }
938 headers
939}
940
941/// Why a request head is malformed under RFC 9114 §4.2, if it is.
942///
943/// `TE` is the one field on the list that is conditionally allowed: §4.2 permits
944/// it when its value is exactly `trailers`, so it is checked separately rather
945/// than banned outright.
946fn connection_specific_field(headers: &HeaderMap) -> Option<&'static str> {
947 for name in CONNECTION_SPECIFIC {
948 if headers.contains_key(name) {
949 return Some(name);
950 }
951 }
952 let te_is_allowed = headers
953 .get_all(http::header::TE)
954 .iter()
955 .all(|v| v.as_bytes().eq_ignore_ascii_case(b"trailers"));
956 (!te_is_allowed).then_some("te")
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962
963 #[test]
964 fn an_absolute_target_becomes_origin_form() {
965 let uri: Uri = "https://example.com/a/b?x=1".parse().unwrap();
966 assert_eq!(
967 normalise_uri(&uri, &Method::GET).to_string(),
968 "/a/b?x=1",
969 "a handler should see the same target on every transport"
970 );
971 }
972
973 #[test]
974 fn an_origin_form_target_is_left_alone() {
975 let uri: Uri = "/a/b".parse().unwrap();
976 assert_eq!(normalise_uri(&uri, &Method::GET).to_string(), "/a/b");
977 }
978
979 #[test]
980 fn a_connect_target_is_left_alone() {
981 let uri: Uri = "example.com:443".parse().unwrap();
982 assert_eq!(
983 normalise_uri(&uri, &Method::CONNECT).to_string(),
984 "example.com:443"
985 );
986 }
987
988 #[test]
989 fn connection_specific_headers_are_dropped() {
990 let mut headers = HeaderMap::new();
991 headers.insert("connection", "keep-alive".parse().unwrap());
992 headers.insert("transfer-encoding", "chunked".parse().unwrap());
993 headers.insert("content-type", "text/plain".parse().unwrap());
994
995 let clean = sanitise(headers);
996 assert!(clean.get("connection").is_none());
997 assert!(clean.get("transfer-encoding").is_none());
998 assert_eq!(clean.get("content-type").unwrap(), "text/plain");
999 }
1000
1001 #[test]
1002 fn a_missing_certificate_is_reported_rather_than_panicking() {
1003 match server_config_from_pem("/nonexistent/cert.pem", "/nonexistent/key.pem") {
1004 Ok(_) => panic!("expected an error for missing files"),
1005 Err(e) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
1006 }
1007 }
1008
1009 #[test]
1010 fn a_configured_keep_alive_becomes_the_quic_idle_bound() {
1011 assert_eq!(idle_ms_for(5_000), 5_000);
1012 }
1013
1014 #[test]
1015 fn a_zero_keep_alive_keeps_the_default_bound() {
1016 // `0` asks TCP to answer and close. QUIC cannot do that, and the
1017 // alternative reading — never expire — would let one idle connection
1018 // hold a `max_connections` slot for good, which is the opposite of what
1019 // the setting asks for.
1020 assert_eq!(idle_ms_for(0), DEFAULT_IDLE_MS);
1021 assert_ne!(idle_ms_for(0), 0);
1022 }
1023
1024 #[test]
1025 fn the_default_bound_agrees_with_the_default_keep_alive() {
1026 // The two constants live in different files; this is what would notice
1027 // if one moved without the other.
1028 assert_eq!(
1029 DEFAULT_IDLE_MS,
1030 crate::app::ServerConfig::default().keep_alive_ms
1031 );
1032 }
1033
1034 #[test]
1035 fn a_configured_stream_limit_becomes_the_quic_bidi_cap() {
1036 assert_eq!(max_streams_for(100), quinn::VarInt::from_u32(100));
1037 }
1038
1039 #[test]
1040 fn a_zero_stream_limit_keeps_the_default_rather_than_advertising_a_maximum() {
1041 // Two ways to get this wrong, and this pins against both.
1042 //
1043 // Passing `0` through forbids every request stream — the opposite of what
1044 // it means on HTTP/2. Translating it to the varint maximum looks right and
1045 // hangs the server: quinn pre-inserts one map entry per advertised remote
1046 // stream when it builds the connection state, so `2^62 - 1` is a
1047 // four-quintillion-iteration loop on the first packet from any peer,
1048 // before anything is authenticated. The bound has to stay small.
1049 assert_eq!(
1050 max_streams_for(0),
1051 quinn::VarInt::from_u32(DEFAULT_MAX_STREAMS)
1052 );
1053 assert_ne!(max_streams_for(0), quinn::VarInt::from_u32(0));
1054 assert_ne!(max_streams_for(0), quinn::VarInt::MAX);
1055 assert!(
1056 u64::from(max_streams_for(0)) <= 100_000,
1057 "an advertised stream cap is allocated against eagerly; it must stay small"
1058 );
1059 }
1060
1061 #[test]
1062 fn no_configured_stream_limit_is_large_enough_to_allocate_against() {
1063 // The property that matters is not about `0` specifically: whatever a
1064 // caller sets, quinn will allocate that many entries per connection.
1065 for configured in [1u32, 200, 10_000] {
1066 assert!(
1067 u64::from(max_streams_for(configured)) <= 100_000,
1068 "{configured} produced a cap quinn would allocate eagerly against"
1069 );
1070 }
1071 }
1072
1073 #[test]
1074 fn the_transport_defaults_come_from_the_app_defaults() {
1075 // What would notice a knob added to `ServerConfig` and wired into the
1076 // TCP engine but never into `TransportLimits`.
1077 let cfg = crate::app::ServerConfig::default();
1078 let limits = TransportLimits::defaults();
1079 assert_eq!(limits.idle_ms, idle_ms_for(cfg.keep_alive_ms));
1080 assert_eq!(
1081 limits.max_streams,
1082 max_streams_for(cfg.h2_max_concurrent_streams)
1083 );
1084 }
1085
1086 #[test]
1087 fn an_ordinary_keep_alive_converts_to_an_idle_timeout() {
1088 assert!(idle_timeout(75_000).is_ok());
1089 }
1090
1091 #[test]
1092 fn a_keep_alive_past_the_varint_range_is_refused_rather_than_panicking() {
1093 // Reachable now that the value is an operator's `u64` rather than a
1094 // constant, so it has to be an error and not an `expect`.
1095 match idle_timeout(u64::MAX) {
1096 Ok(_) => panic!("expected an error for an out-of-range idle timeout"),
1097 Err(e) => {
1098 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1099 assert!(
1100 e.to_string().contains("keep_alive_ms"),
1101 "the error should name the setting at fault, got: {e}"
1102 );
1103 }
1104 }
1105 }
1106}