HTTP/3 and QUIC Support¶
This guide covers HTTP/3 (Hyper profile) support in Quill, built on QUIC transport using the quinn library.
Table of Contents¶
- Overview
- Enabling HTTP/3
- Hyper Profile Configuration
- 0-RTT Support
- HTTP/3 Datagrams
- Connection Migration
- Server Setup
- Client Setup
- TLS Configuration
- Performance Considerations
- Troubleshooting
Overview¶
The Hyper profile provides HTTP/3 transport over QUIC, offering:
- Multiplexed Streams: Independent streams over a single QUIC connection
- 0-RTT Support: Fast connection resumption for idempotent requests
- Connection Migration: Seamless handoff between networks (WiFi ↔ cellular)
- HTTP/3 Datagrams: Unreliable, unordered messaging for real-time data
- Improved Loss Recovery: QUIC's built-in loss detection and recovery
- Head-of-Line Blocking Elimination: Stream-level independence
Why HTTP/3?¶
HTTP/3 is ideal for: - Mobile Clients: Connection migration maintains connectivity during network changes - Browser Applications: Modern browsers support HTTP/3 natively - Lossy Networks: Better performance on networks with packet loss - Real-Time Applications: Datagrams for low-latency messaging - Edge-to-Client Communication: CDN to end-user connections
Enabling HTTP/3¶
HTTP/3 support is behind a feature flag. Enable it in your Cargo.toml:
[dependencies]
quill-transport = { version = "0.1", features = ["http3"] }
quill-client = { version = "0.1", features = ["http3"] }
quill-server = { version = "0.1", features = ["http3"] }
Dependencies¶
The HTTP/3 implementation uses: - quinn: Pure Rust QUIC implementation (v0.11) - h3: HTTP/3 protocol implementation (v0.0.8) - h3-quinn: Integration layer between h3 and quinn - rustls: TLS 1.3 implementation for encryption
Note: The h3 crate is still experimental. While functional, expect potential API changes and bugs.
Hyper Profile Configuration¶
Basic Configuration¶
use quill_transport::{HyperConfig, HyperTransport};
let config = HyperConfig {
enable_zero_rtt: false, // Disabled by default for safety
enable_datagrams: true, // Enable HTTP/3 datagrams
enable_connection_migration: true, // Enable connection migration
max_concurrent_streams: 100, // Max concurrent HTTP/3 streams
max_datagram_size: 65536, // 64 KB datagram limit
keep_alive_interval_ms: 30000, // 30-second keep-alive
idle_timeout_ms: 60000, // 60-second idle timeout
};
let transport = HyperTransport::with_config(config);
Configuration Options¶
| Option | Default | Description |
|---|---|---|
enable_zero_rtt |
false |
Enable 0-RTT for faster connection resumption |
enable_datagrams |
true |
Enable HTTP/3 datagrams for unreliable messaging |
enable_connection_migration |
true |
Allow connections to migrate between networks |
max_concurrent_streams |
100 |
Maximum number of concurrent streams |
max_datagram_size |
65536 |
Maximum datagram payload size (bytes) |
keep_alive_interval_ms |
30000 |
Interval for sending keep-alive packets |
idle_timeout_ms |
60000 |
Connection idle timeout before closing |
0-RTT Support¶
Overview¶
0-RTT (Zero Round-Trip Time) allows clients to send application data in the first flight of packets, reducing latency for connection resumption.
Security Considerations: - Only use 0-RTT for idempotent operations (GET, HEAD, safe RPCs) - Servers must protect against replay attacks - 0-RTT is disabled by default for safety
Enabling 0-RTT¶
Mark RPC methods as idempotent in your .proto files:
service ImageService {
rpc GetMetadata(GetRequest) returns (ImageMetadata) {
option (quill.rpc) = { idempotent: true };
}
}
Enable 0-RTT in the client:
use quill_transport::{H3ClientBuilder};
let client = H3ClientBuilder::new()
.enable_zero_rtt(true) // Enable 0-RTT for idempotent requests
.enable_datagrams(true)
.build()?;
Server 0-RTT Handling¶
Servers automatically detect 0-RTT requests and can reject replays:
use quill_transport::{H3ServerBuilder};
let server = H3ServerBuilder::new("0.0.0.0:4433".parse()?)
.enable_zero_rtt(true)
.build()?;
// The server will return 425 Too Early for replayed 0-RTT requests
0-RTT Replay Protection¶
Quill implements replay protection:
1. Server maintains a rolling window of accepted 0-RTT tickets
2. Non-idempotent methods reject 0-RTT automatically
3. Returns 425 Too Early status code on suspected replays
HTTP/3 Datagrams¶
Overview¶
HTTP/3 datagrams provide unreliable, unordered message delivery: - No delivery guarantees (packets may be lost) - No ordering guarantees (packets may arrive out of order) - Lower latency than streams (no head-of-line blocking)
Use cases: - Real-time sensor data - Gaming updates - Video/audio packets with FEC - Telemetry and metrics
Datagram Type¶
The Datagram type represents a single datagram message:
use quill_transport::Datagram;
use bytes::Bytes;
// Create a simple datagram
let dg = Datagram::new(Bytes::from("sensor:temp=72.5"));
// Create a datagram with flow ID for routing
let dg = Datagram::with_flow_id(Bytes::from("data"), 42);
// Access payload and metadata
println!("Size: {} bytes", dg.size());
println!("Flow ID: {:?}", dg.flow_id);
Client-Side Datagrams¶
Persistent Connection with Datagrams¶
Use connect() to establish a persistent connection for bidirectional datagram exchange:
use quill_transport::{H3ClientBuilder, Datagram};
use bytes::Bytes;
let client = H3ClientBuilder::new()
.enable_datagrams(true)
.build()?;
// Establish persistent connection
let mut conn = client.connect(addr, "example.com").await?;
// Send datagrams
conn.send_datagram(Datagram::new(Bytes::from("hello")))?;
conn.send_datagram(Datagram::with_flow_id(Bytes::from("data"), 1))?;
// Get the datagram sender for concurrent sending
let sender = conn.datagram_sender();
sender.send_bytes(Bytes::from("raw bytes"))?;
// Receive datagrams
if let Some(mut rx) = conn.take_datagram_receiver() {
while let Some(dg) = rx.recv().await {
println!("Received: {:?}", dg.payload);
}
}
One-Shot Datagram Send¶
For sending a single datagram without maintaining a connection:
// Send a datagram without keeping the connection open
client.send_datagram_oneshot(addr, Datagram::new(Bytes::from("fire-and-forget"))).await?;
Server-Side Datagrams¶
Datagram Handler¶
Use FnDatagramHandler or implement DatagramHandler to process incoming datagrams:
use quill_transport::{H3ServerBuilder, FnDatagramHandler, Datagram, DatagramSender};
// Create a datagram handler
let handler = FnDatagramHandler::new(|dg: Datagram, sender: DatagramSender| {
println!("Received datagram: {} bytes", dg.size());
// Echo the datagram back
if let Err(e) = sender.send(dg) {
eprintln!("Failed to send response: {}", e);
}
});
let server = H3ServerBuilder::new(addr)
.enable_datagrams(true)
.build()?;
// Start server with both HTTP/3 request handling and datagram handling
server.serve_with_datagrams(my_service, handler).await?;
Custom Datagram Handler¶
Implement the DatagramHandler trait for more complex logic:
use quill_transport::{DatagramHandler, Datagram, DatagramSender};
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
struct MetricsHandler {
metrics: Arc<Mutex<Vec<f64>>>,
}
impl DatagramHandler for MetricsHandler {
fn handle(&self, datagram: Datagram, _sender: DatagramSender) {
// Parse sensor data from payload
if let Ok(value) = parse_sensor_data(&datagram.payload) {
let metrics = self.metrics.clone();
tokio::spawn(async move {
metrics.lock().await.push(value);
});
}
}
}
Datagram Size Limits¶
let config = HyperConfig {
max_datagram_size: 32768, // 32 KB limit
..Default::default()
};
// Datagrams larger than max_datagram_size will be rejected
// Check the sender's max size before sending
let sender = conn.datagram_sender();
println!("Max datagram size: {} bytes", sender.max_size());
Flow IDs¶
Flow IDs allow multiplexing multiple logical streams over datagrams:
// Encode flow ID in the datagram
let dg = Datagram::with_flow_id(Bytes::from("stream-1-data"), 1);
let encoded = dg.encode(); // [varint flow_id][payload]
// Decode flow ID from received datagram
let decoded = Datagram::decode(received_bytes, true)?;
match decoded.flow_id {
Some(1) => handle_stream_1(decoded.payload),
Some(2) => handle_stream_2(decoded.payload),
_ => handle_default(decoded.payload),
}
Recommendations: - Keep datagrams < 1200 bytes to avoid fragmentation - Use datagrams for data that can tolerate loss - Consider Forward Error Correction (FEC) for important datagram data - Use flow IDs to multiplex different data types over datagrams - Handle datagram loss gracefully in application logic
Connection Migration¶
Overview¶
Connection migration allows QUIC connections to survive network changes: - WiFi ↔ Cellular handoff - IP address changes - NAT rebinding
This is critical for mobile applications and improving user experience.
Enabling Connection Migration¶
let client = H3ClientBuilder::new()
.enable_connection_migration(true)
.build()?;
// Connection will automatically migrate when network changes
How It Works¶
- Client detects network change (new IP address)
- Client sends PATH_CHALLENGE on new path
- Server responds with PATH_RESPONSE
- Connection migrates to new path seamlessly
- Application code sees no interruption
Migration Events¶
Monitor connection migration:
// Connection migration is transparent to application code
// QUIC handles all PATH_CHALLENGE/PATH_RESPONSE exchanges
Quill RPC over HTTP/3¶
QuillH3Client¶
The QuillH3Client provides the same API as QuillClient but uses HTTP/3 transport:
use quill_client::{QuillH3Client, H3ClientConfig};
use bytes::Bytes;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "127.0.0.1:4433".parse()?;
// Create HTTP/3 client with configuration
let client = QuillH3Client::builder(addr)
.enable_zero_rtt(true) // Fast resumption
.enable_compression(true) // zstd compression
.max_concurrent_streams(200)
.build()?;
// Make a unary RPC call
let request = Bytes::from("hello");
let response = client.call("echo.v1.EchoService", "Echo", request).await?;
println!("Response: {:?}", response);
Ok(())
}
Server Streaming over HTTP/3¶
use quill_client::QuillH3Client;
use tokio_stream::StreamExt;
let client = QuillH3Client::builder(addr).build()?;
// Make a server streaming call
let request = Bytes::from(r#"{"query": "logs"}"#);
let mut stream = client
.call_server_streaming("logging.v1.LogService", "TailLogs", request)
.await?;
// Process streaming responses
while let Some(result) = stream.next().await {
match result {
Ok(log_entry) => println!("Log: {:?}", log_entry),
Err(e) => eprintln!("Error: {}", e),
}
}
Client Streaming over HTTP/3¶
use quill_client::QuillH3Client;
use tokio_stream::iter;
let client = QuillH3Client::builder(addr).build()?;
// Create a stream of file chunks
let chunks = vec![
Ok(Bytes::from("chunk1")),
Ok(Bytes::from("chunk2")),
Ok(Bytes::from("chunk3")),
];
let request_stream = Box::pin(iter(chunks));
// Make a client streaming call
let response = client
.call_client_streaming("upload.v1.UploadService", "Upload", request_stream)
.await?;
Bidirectional Streaming over HTTP/3¶
use quill_client::QuillH3Client;
use tokio_stream::StreamExt;
let client = QuillH3Client::builder(addr).build()?;
// Create request stream
let messages = vec![
Ok(Bytes::from(r#"{"text": "Hello"}"#)),
Ok(Bytes::from(r#"{"text": "World"}"#)),
];
let request_stream = Box::pin(tokio_stream::iter(messages));
// Make bidirectional streaming call
let mut response_stream = client
.call_bidi_streaming("chat.v1.ChatService", "Chat", request_stream)
.await?;
while let Some(result) = response_stream.next().await {
match result {
Ok(msg) => println!("Received: {:?}", msg),
Err(e) => eprintln!("Error: {}", e),
}
}
QuillH3Server¶
The QuillH3Server serves Quill RPC methods over HTTP/3:
use quill_server::{QuillH3Server, RpcRouter};
use bytes::Bytes;
use quill_core::QuillError;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "0.0.0.0:4433".parse()?;
// Create server with handlers
let server = QuillH3Server::builder(addr)
.enable_zero_rtt(true)
.enable_datagrams(true)
.max_concurrent_streams(200)
.idle_timeout_ms(120000)
.register("echo.v1.EchoService/Echo", |req: Bytes| async move {
Ok(req) // Echo back
})
.build();
println!("Quill HTTP/3 server listening on {}", server.bind_addr());
// Start serving
server.serve().await?;
Ok(())
}
Server Setup¶
Basic HTTP/3 Server¶
use quill_transport::{H3ServerBuilder, H3Service};
use bytes::Bytes;
use http::{Request, Response, StatusCode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "0.0.0.0:4433".parse()?;
let server = H3ServerBuilder::new(addr)
.enable_zero_rtt(true)
.enable_datagrams(true)
.max_concurrent_streams(200)
.idle_timeout_ms(120000) // 2 minutes
.build()?;
println!("HTTP/3 server listening on {}", server.bind_addr());
// Server loop would go here
// (Full implementation requires quinn endpoint setup)
Ok(())
}
Production Server Configuration¶
let server = H3ServerBuilder::new(addr)
.enable_zero_rtt(true)
.enable_datagrams(true)
.enable_connection_migration(true)
.max_concurrent_streams(1000)
.max_datagram_size: 65536,
.keep_alive_interval_ms(20000) // 20 seconds
.idle_timeout_ms(300000) // 5 minutes
.build()?;
Client Setup¶
Basic HTTP/3 Client¶
use quill_transport::{H3ClientBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = H3ClientBuilder::new()
.enable_zero_rtt(false) // Start with 0-RTT disabled
.enable_datagrams(true)
.build()?;
// Use client for HTTP/3 requests
// (Full implementation requires quinn connection setup)
Ok(())
}
Mobile Client Configuration¶
Optimize for mobile networks:
let client = H3ClientBuilder::new()
.enable_zero_rtt(true) // Fast resumption
.enable_datagrams(true) // Real-time updates
.enable_connection_migration(true) // Network handoff
.build()?;
TLS Configuration¶
Server TLS¶
HTTP/3 requires TLS 1.3. Use rustls for certificate configuration:
use rustls::ServerConfig;
use std::sync::Arc;
// Load server certificate and private key
let certs = load_certs("server.crt")?;
let key = load_private_key("server.key")?;
let mut server_crypto = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
// Enable 0-RTT (requires session storage)
server_crypto.max_early_data_size = 16384;
Client TLS¶
use rustls::ClientConfig;
let mut client_crypto = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// Enable 0-RTT
client_crypto.enable_early_data = true;
Self-Signed Certificates (Development)¶
For development, accept self-signed certificates:
// WARNING: Only use in development!
let client_crypto = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
Performance Considerations¶
Latency Benefits¶
HTTP/3 provides significant latency improvements:
| Scenario | HTTP/2 | HTTP/3 | Improvement |
|---|---|---|---|
| First connection | 2-3 RTT | 1-2 RTT | ~33-50% |
| Resumption (0-RTT) | 1 RTT | 0 RTT | ~100% |
| Packet loss (1%) | +200ms | +50ms | ~75% |
Throughput¶
QUIC/HTTP/3 throughput is comparable to HTTP/2: - Low loss: ~same throughput - High loss (>1%): HTTP/3 is 20-50% faster - Mobile networks: HTTP/3 significantly better
Resource Usage¶
- Memory: ~10-20% more than HTTP/2 (QUIC state)
- CPU: ~5-15% more for encryption (TLS 1.3 per packet)
- Battery: Connection migration reduces reconnections
Optimization Tips¶
- Enable 0-RTT for idempotent requests (50-100ms saved)
- Use datagrams for real-time data (lower latency)
- Tune max_concurrent_streams based on workload
- Adjust keep_alive_interval for mobile (conserve battery)
- Monitor connection migration events
Troubleshooting¶
Common Issues¶
1. QUIC Blocked by Firewall¶
Problem: QUIC uses UDP on port 443/4433, often blocked by corporate firewalls.
Solution: - Fallback to HTTP/2 (Turbo profile) if QUIC fails - Use QUIC on alternate ports (e.g., 8443) - Configure firewall to allow UDP
2. 0-RTT Replay Attacks¶
Problem: 0-RTT requests are replayed by attackers.
Solution:
- Only enable 0-RTT for idempotent operations
- Return 425 Too Early for suspected replays
- Implement replay detection (ticket window)
3. Datagram Loss¶
Problem: Datagrams are lost frequently.
Solution: - Reduce datagram size (< 1200 bytes) - Implement application-level reliability (FEC, retransmission) - Use streams for reliable delivery
4. Connection Migration Failures¶
Problem: Connections fail to migrate between networks.
Solution:
- Verify enable_connection_migration is true
- Check NAT traversal (some NATs block migration)
- Increase idle_timeout_ms to prevent premature closure
Debugging¶
Enable QUIC debugging logs:
use tracing::Level;
use tracing_subscriber;
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
// QUIC events will be logged to stdout
Monitor QUIC metrics:
// Connection statistics
let stats = connection.stats();
println!("Path MTU: {}", stats.path.mtu);
println!("RTT: {:?}", stats.path.rtt);
println!("Lost packets: {}", stats.path.lost_packets);
Browser Compatibility¶
Modern browsers support HTTP/3: - Chrome 87+ (stable support) - Firefox 88+ (enabled by default) - Safari 14+ (experimental) - Edge 87+ (same as Chrome)
Check HTTP/3 in browser DevTools: - Chrome: Protocol column shows "h3" or "h3-29" - Firefox: Protocol column shows "HTTP/3"
Security Best Practices¶
- Always use TLS 1.3 (QUIC requires it)
- Disable 0-RTT for non-idempotent operations
- Implement replay detection for 0-RTT
- Validate certificates properly (no self-signed in production)
- Monitor for QUIC-specific attacks (amplification, migration abuse)
Migration from HTTP/2¶
Profile Negotiation¶
Quill automatically negotiates the best profile:
Client: Prefer: prism=hyper,turbo,classic
Server: Selected-Prism: hyper (or turbo if HTTP/3 unavailable)
Gradual Rollout¶
- Phase 1: Enable HTTP/3 for development/staging
- Phase 2: Enable for small percentage of production traffic
- Phase 3: Monitor latency, throughput, error rates
- Phase 4: Gradually increase HTTP/3 traffic
- Phase 5: Make HTTP/3 default, fallback to HTTP/2
See Also¶
- HTTP/2 Configuration - Turbo profile documentation
- Resilience Guide - Retry and circuit breaker patterns
- Performance Guide - Performance optimization