HTTP/3 Server
QuicD includes built-in HTTP/3 support via the quicd-h3 crate. This guide shows you how to run an HTTP/3 server and implement custom request handlers.
What is HTTP/3?
Section titled “What is HTTP/3?”HTTP/3 is the latest version of HTTP, built on QUIC instead of TCP. Key benefits:
- Faster connection setup: 0-RTT connection resumption
- No head-of-line blocking: Independent stream processing
- Better loss recovery: Stream-level retransmission
- Connection migration: Survives network changes (Wi-Fi → cellular)
QuicD implements HTTP/3 (RFC 9114) with QPACK header compression (RFC 9204).
Quick Start
Section titled “Quick Start”1. Create Configuration
Section titled “1. Create Configuration”Create config.toml:
host = "0.0.0.0"port = 8443log_level = "info"
[netio]workers = 4
[quic]max_connections_per_worker = 100002. Run Server
Section titled “2. Run Server”sudo cargo run --releaseQuicD automatically registers HTTP/3 for ALPN "h3" and "h3-29".
Expected output:
[INFO] Application registry initialized: alpns=["h3", "h3-29"][INFO] Network IO layer started: addr=0.0.0.0:8443, workers=43. Test with Example Client
Section titled “3. Test with Example Client”cargo run --example h3_clientClient output:
✓ Handshake completed! ALPN: h3✓ Sent 23 bytes on stream 0✓ Received echo: "Hello from test client!"Congratulations! You’re running HTTP/3.
Default Handler
Section titled “Default Handler”QuicD ships with DefaultH3Handler, a simple echo handler for testing:
// Built into quicd-h3pub struct DefaultH3Handler;
#[async_trait]impl H3Handler for DefaultH3Handler { async fn handle_request( &self, request: H3Request, response: H3ResponseSender, ) -> Result<(), H3Error> { // Echo request info back as response body let body = format!("{} {}", request.method, request.path); response.send_response(200, vec![], Some(Bytes::from(body))).await }}This is registered automatically in main.rs:
let registry = AppRegistry::new() .register("h3", Arc::new(H3Factory::new(DefaultH3Handler))) .register("h3-29", Arc::new(H3Factory::new(DefaultH3Handler)));Custom Request Handler
Section titled “Custom Request Handler”Implement H3Handler to build your own HTTP/3 application.
H3Handler Trait
Section titled “H3Handler Trait”#[async_trait]pub trait H3Handler: Send + Sync + 'static { async fn handle_request( &self, request: H3Request, response: H3ResponseSender, ) -> Result<(), H3Error>;}H3Request Structure
Section titled “H3Request Structure”pub struct H3Request { pub method: String, // "GET", "POST", etc. pub scheme: String, // "https" pub authority: String, // "example.com:8443" pub path: String, // "/index.html" pub headers: Vec<(String, String)>, pub body: RecvStream, // Zero-copy body stream}H3ResponseSender API
Section titled “H3ResponseSender API”impl H3ResponseSender { pub async fn send_response( &self, status: u16, headers: Vec<(String, String)>, body: Option<Bytes>, ) -> Result<(), H3Error>;
pub async fn send_response_streaming( &self, status: u16, headers: Vec<(String, String)>, ) -> Result<SendStream, H3Error>;}Example: Static File Server
Section titled “Example: Static File Server”use async_trait::async_trait;use bytes::Bytes;use quicd_h3::{H3Error, H3Handler, H3Request, H3ResponseSender};use std::path::PathBuf;use tokio::fs;
pub struct FileHandler { root: PathBuf,}
impl FileHandler { pub fn new(root: impl Into<PathBuf>) -> Self { Self { root: root.into() } }}
#[async_trait]impl H3Handler for FileHandler { async fn handle_request( &self, request: H3Request, response: H3ResponseSender, ) -> Result<(), H3Error> { // Only handle GET if request.method != "GET" { return response.send_response( 405, vec![("allow".into(), "GET".into())], Some(Bytes::from("Method Not Allowed")), ).await; }
// Construct file path (basic, no security checks!) let file_path = self.root.join(request.path.trim_start_matches('/'));
// Read file match fs::read(&file_path).await { Ok(contents) => { // Detect content type let content_type = match file_path.extension().and_then(|s| s.to_str()) { Some("html") => "text/html", Some("css") => "text/css", Some("js") => "application/javascript", Some("json") => "application/json", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", _ => "application/octet-stream", };
response.send_response( 200, vec![ ("content-type".into(), content_type.into()), ("content-length".into(), contents.len().to_string()), ], Some(Bytes::from(contents)), ).await } Err(_) => { response.send_response( 404, vec![], Some(Bytes::from("Not Found")), ).await } } }}Register Custom Handler
Section titled “Register Custom Handler”Modify quicd/src/main.rs:
// Add importuse your_crate::FileHandler;
// In main()let app_registry = apps::AppRegistry::new() .register("h3", Arc::new(quicd_h3::H3Factory::new( FileHandler::new("./www") // Serve from ./www directory ))) .register("h3-29", Arc::new(quicd_h3::H3Factory::new( FileHandler::new("./www") )));Example: JSON API
Section titled “Example: JSON API”use async_trait::async_trait;use bytes::Bytes;use quicd_h3::{H3Error, H3Handler, H3Request, H3ResponseSender};use serde::{Deserialize, Serialize};use serde_json;
#[derive(Deserialize)]struct CreateUserRequest { username: String, email: String,}
#[derive(Serialize)]struct CreateUserResponse { id: u64, username: String, email: String,}
pub struct ApiHandler;
#[async_trait]impl H3Handler for ApiHandler { async fn handle_request( &self, mut request: H3Request, response: H3ResponseSender, ) -> Result<(), H3Error> { match (request.method.as_str(), request.path.as_str()) { ("POST", "/api/users") => { // Read request body let mut body_bytes = Vec::new(); while let Ok(Some(chunk)) = request.body.read().await { match chunk { quicd_x::StreamData::Data(data) => body_bytes.extend_from_slice(&data), quicd_x::StreamData::Fin => break, } }
// Parse JSON let create_req: CreateUserRequest = serde_json::from_slice(&body_bytes) .map_err(|_| H3Error::Protocol("Invalid JSON".into()))?;
// Create user (mock) let user = CreateUserResponse { id: 123, username: create_req.username, email: create_req.email, };
// Send JSON response let response_json = serde_json::to_vec(&user) .map_err(|_| H3Error::Protocol("JSON serialization failed".into()))?;
response.send_response( 201, vec![ ("content-type".into(), "application/json".into()), ], Some(Bytes::from(response_json)), ).await } ("GET", path) if path.starts_with("/api/users/") => { // Extract user ID from path let user_id = path.trim_start_matches("/api/users/");
// Mock response let user = CreateUserResponse { id: user_id.parse().unwrap_or(0), username: "john_doe".into(), email: "john@example.com".into(), };
let response_json = serde_json::to_vec(&user) .map_err(|_| H3Error::Protocol("JSON serialization failed".into()))?;
response.send_response( 200, vec![("content-type".into(), "application/json".into())], Some(Bytes::from(response_json)), ).await } _ => { response.send_response( 404, vec![], Some(Bytes::from("Not Found")), ).await } } }}Streaming Responses
Section titled “Streaming Responses”For large responses (video, logs), use streaming:
#[async_trait]impl H3Handler for StreamingHandler { async fn handle_request( &self, request: H3Request, response: H3ResponseSender, ) -> Result<(), H3Error> { if request.path == "/stream" { // Get streaming send handle let send_stream = response.send_response_streaming( 200, vec![("content-type".into(), "text/plain".into())], ).await?;
// Stream data in chunks for i in 0..10 { let chunk = format!("Chunk {}\n", i); send_stream.write(Bytes::from(chunk), false).await .map_err(|e| H3Error::Connection(format!("write error: {:?}", e)))?;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; }
// Send FIN send_stream.finish().await .map_err(|e| H3Error::Connection(format!("finish error: {:?}", e)))?;
Ok(()) } else { response.send_response(404, vec![], Some(Bytes::from("Not Found"))).await } }}Reading Request Body
Section titled “Reading Request Body”For POST/PUT requests with body data:
async fn read_body(mut recv_stream: RecvStream) -> Result<Vec<u8>, H3Error> { let mut body = Vec::new();
loop { match recv_stream.read().await { Ok(Some(quicd_x::StreamData::Data(chunk))) => { body.extend_from_slice(&chunk); } Ok(Some(quicd_x::StreamData::Fin)) => break, Ok(None) => break, Err(e) => return Err(H3Error::Connection(format!("read error: {:?}", e))), } }
Ok(body)}Advanced: Middleware Pattern
Section titled “Advanced: Middleware Pattern”use std::sync::Arc;
pub struct MiddlewareChain<H: H3Handler> { inner: Arc<H>, middlewares: Vec<Arc<dyn Middleware>>,}
#[async_trait]pub trait Middleware: Send + Sync { async fn process( &self, request: &mut H3Request, ) -> Result<Option<H3Response>, H3Error>;}
// Example: Authentication middlewarepub struct AuthMiddleware { api_key: String,}
#[async_trait]impl Middleware for AuthMiddleware { async fn process(&self, request: &mut H3Request) -> Result<Option<H3Response>, H3Error> { let auth_header = request.headers.iter() .find(|(k, _)| k == "authorization") .map(|(_, v)| v);
if let Some(auth) = auth_header { if auth == &format!("Bearer {}", self.api_key) { return Ok(None); // Continue to handler } }
// Return 401 Unauthorized Ok(Some(H3Response { status: 401, headers: vec![("www-authenticate".into(), "Bearer".into())], body: Some(Bytes::from("Unauthorized")), })) }}Testing HTTP/3 Server
Section titled “Testing HTTP/3 Server”With curl (HTTP/3 build)
Section titled “With curl (HTTP/3 build)”# Build curl with HTTP/3 support# See https://github.com/curl/curl/blob/master/HTTP3.md
curl --http3 https://localhost:8443/With quiche-client
Section titled “With quiche-client”cargo install quiche-clientquiche-client https://localhost:8443/With Browser
Section titled “With Browser”Modern browsers support HTTP/3:
- Chrome/Edge: Enable
chrome://flags/#enable-quic - Firefox: Set
network.http.http3.enabledtotrue
Note: Browsers won’t accept self-signed certificates. Use proper CA-signed certs for testing.
Performance Considerations
Section titled “Performance Considerations”Connection Pooling
Section titled “Connection Pooling”HTTP/3 connections are persistent and multiplexed. Clients should reuse connections:
// Client-side (conceptual)let connection = connect_to_server().await?;
// Send multiple requests on same connectionfor path in ["/api/users", "/api/posts", "/api/comments"] { let stream = connection.open_bi().await?; send_request(stream, path).await?;}Flow Control Tuning
Section titled “Flow Control Tuning”For high-throughput APIs, increase flow control limits:
[quic]initial_max_data = 50000000 # 50MB for large responsesinitial_max_stream_data_bidi_remote = 10000000 # 10MB per streamRequest Concurrency
Section titled “Request Concurrency”HTTP/3 allows many concurrent requests. Tune stream limits:
[quic]initial_max_streams_bidi = 500 # Support 500 concurrent requestsCommon Patterns
Section titled “Common Patterns”Routing
Section titled “Routing”Simple path-based routing:
match request.path.as_str() { "/" => handle_index(response).await, "/api/users" => handle_users(request, response).await, path if path.starts_with("/static/") => serve_static(path, response).await, _ => response.send_response(404, vec![], Some(Bytes::from("Not Found"))).await,}Error Handling
Section titled “Error Handling”match handle_request_logic(&request).await { Ok(body) => response.send_response(200, vec![], Some(body)).await, Err(e) => { eprintln!("Error: {:?}", e); response.send_response( 500, vec![], Some(Bytes::from("Internal Server Error")), ).await }}Headers
Section titled “Headers”// Set custom headerslet headers = vec![ ("content-type".into(), "application/json".into()), ("cache-control".into(), "max-age=3600".into()), ("x-custom-header".into(), "value".into()),];
response.send_response(200, headers, Some(body)).awaitNext Steps
Section titled “Next Steps”- Build Custom Apps: Implement non-HTTP/3 protocols
- Configuration: Tune performance
- Architecture: Understand internals
HTTP/3 on QuicD is production-ready. Build fast, modern web applications with zero head-of-line blocking.