h3_axum/
lib.rs

1//! # h3-axum
2//!
3//! Transport your Axum router over HTTP/3.
4//!
5//! Use your existing Axum handlers, extractors, and middleware with HTTP/3/QUIC
6//! without changing your application code.
7//!
8//! ## Quick Start
9//!
10//! ```ignore
11//! use h3_axum::serve_h3_with_axum;
12//!
13//! // Your normal Axum router (unchanged!)
14//! let app = Router::new()
15//!     .route("/", get(handler));
16//!
17//! // Serve it over H3 (one line)
18//! serve_h3_with_axum(app, resolver).await?;
19//! ```
20
21#![forbid(unsafe_code)]
22#![warn(missing_docs)]
23
24use std::error::Error;
25
26use bytes::{Buf, Bytes};
27use http::{Request, Response};
28use http_body_util;
29
30/// Boxed error type
31pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
32
33/// Check if an H3 connection error represents a graceful close.
34///
35/// HTTP/3 and QUIC have multiple ways to signal graceful connection closure.
36/// This function identifies them to avoid logging benign closes as errors.
37///
38/// # Example
39///
40/// ```ignore
41/// match h3_conn.accept().await {
42///     Err(e) if is_graceful_h3_close(&e) => {
43///         tracing::debug!("Connection closed gracefully");
44///     }
45///     Err(e) => {
46///         tracing::error!("Connection error: {:?}", e);
47///     }
48///     // ...
49/// }
50/// ```
51pub fn is_graceful_h3_close(err: &h3::error::ConnectionError) -> bool {
52    // Check error string representation for graceful close patterns
53    // Since h3 error types are private/non-exhaustive, string matching is idiomatic.
54    // Common graceful close patterns from production:
55    // - Remote(Undefined(ConnectionClosed { error_code: NO_ERROR, ... }))
56    // - ApplicationClose: 0x0 - QUIC NO_ERROR application close
57    let err_debug = format!("{:?}", err);
58
59    if err_debug.contains("NO_ERROR")
60        || err_debug.contains("ApplicationClose: 0x0")
61        || err_debug.contains("ApplicationClose(0x0)")
62        || err_debug.contains("ConnectionClosed")
63    {
64        return true;
65    }
66
67    // Walk error source chain for typed QUIC-level causes
68    let mut cur: &(dyn std::error::Error + 'static) = err;
69    while let Some(src) = cur.source() {
70        let src_debug = format!("{:?}", src);
71        if src_debug.contains("NO_ERROR") || src_debug.contains("ApplicationClose") {
72            return true;
73        }
74        cur = src;
75    }
76
77    false
78}
79
80/// Serve an Axum Router over an H3 request.
81///
82/// This is the main function that bridges your existing Axum Router to HTTP/3.
83/// It handles the H3 protocol details so your service doesn't have to.
84///
85/// # Example
86///
87/// ```ignore
88/// use axum::{Router, routing::get};
89/// use h3_axum::serve_h3_with_axum;
90///
91/// let app = Router::new()
92///     .route("/", get(|| async { "Hello H3!" }));
93///
94/// // When you get an H3 request:
95/// serve_h3_with_axum(app, resolver).await?;
96/// ```
97pub async fn serve_h3_with_axum<Q>(
98    app: axum::Router,
99    resolver: h3::server::RequestResolver<Q, Bytes>,
100) -> Result<(), BoxError>
101where
102    Q: h3::quic::Connection<Bytes>,
103{
104    // Resolve the H3 request
105    let (request_head, mut stream) = resolver.resolve_request().await?;
106
107    // Read request body from H3
108    let mut body_bytes = bytes::BytesMut::new();
109    loop {
110        match stream.recv_data().await {
111            Ok(Some(mut chunk)) => {
112                body_bytes.extend_from_slice(&chunk.copy_to_bytes(chunk.remaining()));
113            }
114            Ok(None) => break,
115            Err(e) => {
116                // Send 400 Bad Request on body read error
117                let mut error_response: Response<()> = Response::new(());
118                *error_response.status_mut() = http::StatusCode::BAD_REQUEST;
119                let _ = stream.send_response(error_response).await;
120                let _ = stream.finish().await;
121                return Err(Box::new(e));
122            }
123        }
124    }
125
126    // Build Axum request
127    let (parts, _) = request_head.into_parts();
128    let axum_req = Request::from_parts(parts, axum::body::Body::from(body_bytes.freeze()));
129
130    // Call Axum router
131    let axum_resp = tower::ServiceExt::oneshot(app, axum_req).await?;
132
133    // Send response back over H3
134    let (parts, axum_body) = axum_resp.into_parts();
135    let head_only: Response<()> = Response::from_parts(parts, ());
136    stream.send_response(head_only).await?;
137
138    // Stream response body chunk by chunk for SSE support
139    let mut body_stream = std::pin::pin!(axum_body);
140    while let Some(frame_result) = http_body_util::BodyExt::frame(&mut body_stream).await {
141        let frame = frame_result?;
142        if let Some(chunk) = frame.data_ref() {
143            if !chunk.is_empty() {
144                stream.send_data(chunk.clone().into()).await?;
145            }
146        }
147    }
148
149    stream.finish().await?;
150
151    Ok(())
152}