JetStream HTTP
JetStream HTTP provides HTTP/2 and HTTP/3 server support with a unified Axum router interface. It enables serving both protocols on the same port using TCP for HTTP/2 and UDP (QUIC) for HTTP/3.
Features
- HTTP/2 + HTTP/3: Serve both protocols simultaneously on the same port
- Axum Integration: Use familiar Axum routers and handlers
- Alt-Svc Header: Automatically advertise HTTP/3 availability to HTTP/2 clients
- Context Extractor: Access connection metadata (peer certificates, remote address) in handlers
- mTLS Support: Mutual TLS authentication for both protocols
Architecture
The crate provides these components:
H3Service: HTTP/3 protocol handler for use withjetstream_quicAltSvcLayer: Tower layer that addsAlt-Svcheader to advertise HTTP/3JetStreamContext: Axum extractor for accessing connection context
Example
Here’s a complete example serving HTTP/2 and HTTP/3:
use std::fs;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use axum::Router;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder as HttpBuilder;
use hyper_util::service::TowerToHyperService;
use jetstream::prelude::*;
use jetstream_http::{AltSvcLayer, H3Service};
use jetstream_macros::service;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
use tower_http::services::ServeDir;
use tracing::{error, info};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
// r[impl jetstream.webtransport.http-example]
#[service]
pub trait EchoHttp {
async fn ping(&mut self, message: String) -> Result<String>;
async fn add(&mut self, a: i32, b: i32) -> Result<i32>;
}
#[derive(Clone)]
struct EchoHttpImpl;
impl EchoHttp for EchoHttpImpl {
async fn ping(&mut self, message: String) -> Result<String> {
Ok(message)
}
async fn add(&mut self, a: i32, b: i32) -> Result<i32> {
Ok(a + b)
}
}
pub static CA_PEM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/certs/ca.pem");
pub static SERVER_PEM: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/certs/server.pem");
pub static SERVER_KEY: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/certs/server.key");
fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
let data = fs::read(Path::new(path)).expect("Failed to read cert");
rustls_pemfile::certs(&mut &*data)
.filter_map(|r| r.ok())
.collect()
}
fn load_key(path: &str) -> PrivateKeyDer<'static> {
let data = fs::read(Path::new(path)).expect("Failed to read key");
rustls_pemfile::private_key(&mut &*data)
.expect("Failed to parse key")
.expect("No key found")
}
pub static APP_DIST: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/examples/app/dist");
/// Run HTTP/2 server with TLS
async fn run_http2_server(
addr: SocketAddr,
router: Router,
tls_acceptor: TlsAcceptor,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind(addr).await?;
loop {
let (stream, _remote_addr) = listener.accept().await?;
let tls_acceptor = tls_acceptor.clone();
let router = router.clone();
tokio::spawn(async move {
let tls_stream = match tls_acceptor.accept(stream).await {
Ok(s) => s,
Err(e) => {
error!("TLS accept error: {}", e);
return;
}
};
let io = TokioIo::new(tls_stream);
let svc = TowerToHyperService::new(router);
if let Err(e) = HttpBuilder::new(TokioExecutor::new())
.serve_connection(io, svc)
.await
{
error!("HTTP/2 connection error: {}", e);
}
});
}
}
/// Run HTTP/3 server with QUIC + WebTransport
async fn run_http3_server(
addr: SocketAddr,
router: Router,
server_cert: CertificateDer<'static>,
server_key: PrivateKeyDer<'static>,
ca_cert: Option<CertificateDer<'static>>,
) {
// Register the EchoHttp service as a WebTransport handler
let echo = echohttp_protocol::EchoHttpService {
inner: EchoHttpImpl,
};
let h3_service = Arc::new(
H3Service::new(router)
.with_handler(echohttp_protocol::PROTOCOL_NAME, echo),
);
let mut quic_router = jetstream_quic::Router::new();
quic_router.register(h3_service);
let server = if let Some(ca) = ca_cert {
let mut root_store = rustls::RootCertStore::empty();
root_store.add(ca).expect("Failed to add CA cert");
let client_verifier =
rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
.allow_unauthenticated()
.build()
.expect("Failed to build client verifier");
jetstream_quic::Server::new_with_mtls(
server_cert,
server_key,
client_verifier,
addr,
quic_router,
)
} else {
jetstream_quic::Server::new_with_addr(
server_cert,
server_key,
addr,
quic_router,
)
};
server.run().await;
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::fmt::init();
// Install the ring crypto provider for rustls
rustls::crypto::ring::default_provider()
.install_default()
.ok();
// Check for --mtls flag
let mtls_enabled = std::env::args().any(|arg| arg == "--mtls");
let server_certs = load_certs(SERVER_PEM);
let server_key = load_key(SERVER_KEY);
let ca_cert = if mtls_enabled {
Some(load_certs(CA_PEM).pop().unwrap())
} else {
None
};
// Create shared Axum router serving React app build + Alt-Svc header
let router = Router::new()
.fallback_service(ServeDir::new(APP_DIST))
.layer(AltSvcLayer::new(4433));
// Setup TLS config for HTTP/2
let mut tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(server_certs.clone(), server_key.clone_key())?;
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));
let addr: SocketAddr = "127.0.0.1:4433".parse()?;
info!("=== JetStream HTTP + WebTransport Server ===");
info!("Listening on https://{}", addr);
info!(" - HTTP/2 over TLS (TCP)");
info!(" - HTTP/3 + WebTransport over QUIC (UDP)");
info!(
" - WebTransport protocol: {}",
echohttp_protocol::PROTOCOL_VERSION
);
if mtls_enabled {
info!("mTLS enabled - client certificates required");
} else {
info!("No client auth (use --mtls to enable)");
}
// Run both servers concurrently on the same port (TCP for HTTP/2, UDP for HTTP/3)
tokio::select! {
result = run_http2_server(addr, router.clone(), tls_acceptor) => {
if let Err(e) = result {
error!("HTTP/2 server error: {}", e);
}
}
_ = run_http3_server(
addr,
router,
server_certs.into_iter().next().unwrap(),
server_key,
ca_cert,
) => {}
}
Ok(())
}
Quick Start
1. Create an Axum Router
#![allow(unused)]
fn main() {
use axum::{routing::get, Router};
use jetstream_http::{AltSvcLayer, JetStreamContext};
async fn handler(ctx: JetStreamContext) -> &'static str {
println!("Request from: {:?}", ctx.remote());
"Hello, World!"
}
let router = Router::new()
.route("/", get(handler))
.layer(AltSvcLayer::new(4433));
}
2. Set Up HTTP/2 Server (TCP + TLS)
#![allow(unused)]
fn main() {
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder as HttpBuilder;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
let listener = TcpListener::bind("127.0.0.1:4433").await?;
let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));
loop {
let (stream, _) = listener.accept().await?;
let tls_stream = tls_acceptor.accept(stream).await?;
let io = TokioIo::new(tls_stream);
let svc = TowerToHyperService::new(router.clone());
HttpBuilder::new(TokioExecutor::new())
.serve_connection(io, svc)
.await?;
}
}
3. Set Up HTTP/3 Server (QUIC)
#![allow(unused)]
fn main() {
use jetstream_http::H3Service;
use jetstream_quic::{Router as QuicRouter, Server};
let h3_service = Arc::new(H3Service::new(router));
let mut quic_router = QuicRouter::new();
quic_router.register(h3_service);
let server = Server::new_with_addr(cert, key, addr, quic_router);
server.run().await;
}
Alt-Svc Layer
The AltSvcLayer adds the Alt-Svc header to HTTP/2 responses, telling browsers that HTTP/3 is available:
#![allow(unused)]
fn main() {
use jetstream_http::AltSvcLayer;
// Advertise HTTP/3 on port 4433 with 24-hour max-age
let layer = AltSvcLayer::new(4433);
// Adds: Alt-Svc: h3=":4433"; ma=86400
// Or with custom value
let layer = AltSvcLayer::with_value("h3=\":443\"; ma=3600, h3-29=\":443\"");
}
When browsers receive this header over HTTP/2, they will attempt to upgrade to HTTP/3 on subsequent requests.
Context Extractor
Use JetStreamContext to access connection metadata in your handlers:
#![allow(unused)]
fn main() {
use jetstream_http::JetStreamContext;
use jetstream_rpc::context::{Peer, RemoteAddr};
async fn handler(ctx: JetStreamContext) -> String {
// Get remote address
let addr = match ctx.remote() {
Some(RemoteAddr::IpAddr(ip)) => ip.to_string(),
_ => "unknown".to_string(),
};
// Get peer certificate info (for mTLS)
if let Some(Peer::Tls(tls_peer)) = ctx.peer() {
if let Some(leaf) = tls_peer.leaf() {
return format!(
"Hello {}! (CN: {:?})",
addr,
leaf.common_name
);
}
}
format!("Hello {}!", addr)
}
}
TLS Configuration
HTTP/2 TLS Config
#![allow(unused)]
fn main() {
let mut tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
// Important: set ALPN for HTTP/2
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
}
HTTP/3 (QUIC) TLS
QUIC/HTTP/3 TLS is handled automatically by jetstream_quic::Server.
Certificates
Generate development certificates:
cd certs
./generate_certs.sh
Files created:
server.pem/server.key- Server certificateca.pem- CA certificate (for client trust)client.pem/client.key- Client certificate (for mTLS)client.p12- PKCS12 bundle for browser importserver.crt- DER format for Chrome
Testing with Chrome
Chrome supports HTTP/3. To test:
# Start the server
cargo run --example http --features quic
# Launch Chrome with QUIC enabled
./examples/launch_chrome.sh
The launch_chrome.sh script:
- Extracts the server’s SPKI hash
- Launches Chrome with
--origin-to-force-quic-onflag - Provides instructions for importing certificates
Dependencies
Add to your Cargo.toml:
[dependencies]
jetstream_http = "0.1"
jetstream_quic = "13"
axum = "0.8"
hyper-util = { version = "0.1", features = ["full"] }
tokio-rustls = "0.26"
tower = "0.5"
For more details, see the jetstream_http API documentation.