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}