Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 with jetstream_quic
  • AltSvcLayer: Tower layer that adds Alt-Svc header to advertise HTTP/3
  • JetStreamContext: 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 certificate
  • ca.pem - CA certificate (for client trust)
  • client.pem / client.key - Client certificate (for mTLS)
  • client.p12 - PKCS12 bundle for browser import
  • server.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:

  1. Extracts the server’s SPKI hash
  2. Launches Chrome with --origin-to-force-quic-on flag
  3. 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.