HTTP/3
Serve HTTP/3 over QUIC on Tokio (quinn) or Compio (compio-quic) with mandatory TLS, congestion control, and stream caps.
HTTP/3 runs over QUIC (UDP) with the h3 crate for the protocol: quinn drives
QUIC on Tokio and compio-quic on Compio. It is gated behind the http3 feature.
The same Router you use for HTTP/1.1 and HTTP/2 drives HTTP/3 unchanged, and
handlers see a ConnInfo whose transport is Http3.
TLS is mandatory for QUIC, so a TlsCert is always required. On Compio a
request body is bound to the runtime thread that received it, like every compio
task; moving it to another thread panics. Enable webtransport to also accept
WebTransport sessions on the same server.
Quickstart
QUIC binds a UDP socket, not a TcpListener, so the address is passed at spawn
time. spawn_h3 returns a handle for startup errors and shutdown; try_spawn_h3
binds before returning, so certificate and bind errors surface immediately.
use anyhow::Result;
use tako::Method;
use tako::responder::Responder;
use tako::router::Router;
async fn hello() -> impl Responder {
"Hello, HTTP/3 World!".into_response()
}
#[tokio::main]
async fn main() -> Result<()> {
let mut router = Router::new();
router.route(Method::GET, "/", hello);
tako::Server::builder()
.tls(tako::TlsCert::pem_paths("cert.pem", "key.pem"))
.build().spawn_h3("[::]:4433", router).result().await?;
Ok(())
}use tako::router::Router;
use tako::types::BoxError;
use tako::{CompioServer, TlsCert};
#[compio::main]
async fn main() -> Result<(), BoxError> {
let mut router = Router::new();
router.get("/", || async { "Hello, HTTP/3 World!" });
let handle = CompioServer::builder()
.tls(TlsCert::pem_paths("cert.pem", "key.pem"))
.build()
.try_spawn_h3("[::]:4433", router)?;
handle.result().await?;
Ok(())
}Test with a recent curl:
curl --http3 -k https://localhost:4433/use anyhow::Result;
use tako::Method;
use tako::responder::Responder;
use tako::router::Router;
async fn hello_world() -> impl Responder {
"Hello, HTTP/3 World!".into_response()
}
async fn json_example() -> impl Responder {
r#"{"message": "Hello from HTTP/3!", "protocol": "h3"}"#.into_response()
}
#[tokio::main]
async fn main() -> Result<()> {
// HTTP/3 requires TLS certificates
// You can generate self-signed certificates for testing with:
// openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
let mut router = Router::new();
router.route(Method::GET, "/", hello_world);
router.route(Method::GET, "/json", json_example);
println!("Starting HTTP/3 server on [::]:4433");
println!("Test with: curl --http3 -k https://localhost:4433/");
tako::Server::builder()
.tls(tako::TlsCert::pem_paths("cert.pem", "key.pem"))
.build()
.spawn_h3("[::]:4433", router)
.result()
.await
.expect("HTTP/3 server failed");
Ok(())
}The Server builder
For graceful drain and an owned shutdown handle, build a Server, attach a
TlsCert, and call spawn_h3 with the bind address. It returns the same
ServerHandle as every other transport.
use tako::{Server, TlsCert};
use tako::router::Router;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut router = Router::new();
router.route(tako::Method::GET, "/", || async { "Hello over HTTP/3" });
let server = Server::builder()
.tls(TlsCert::pem_paths("cert.pem", "key.pem"))
.build();
let handle = server.spawn_h3("[::]:4433", router);
handle.join().await;
Ok(())
}Without a TlsCert, spawn_h3 reports the error through result() and
try_spawn_h3 returns it immediately — QUIC cannot run without TLS. Every
TlsCert variant (PEM paths, DER, Resolver, mTLS) flows through the same
rustls-config helper as HTTPS.
Tuning QUIC
HTTP/3 knobs live on ServerConfig alongside the HTTP/1 and HTTP/2 settings and
apply on both runtimes:
| Field | Purpose |
|---|---|
h3_max_concurrent_bidi_streams | Cap on client-initiated bidirectional streams (default 100). |
h3_max_concurrent_uni_streams | Cap on client-initiated unidirectional streams (default 8). |
h3_max_idle_timeout | Idle timeout with no QUIC packets either direction (default 30 s). |
h3_congestion | Congestion controller: Cubic (default), NewReno, or Bbr. |
h3_enable_datagrams | Enable QUIC datagrams (RFC 9221) on the connection. |
h3_use_retry | Issue a QUIC Retry packet to validate source addresses (anti-amplification). |
h3_goaway_grace | Per-connection grace for in-flight streams after GOAWAY. |
use tako::{Server, ServerConfig, TlsCert};
use tako::router::Router;
use tako_rs_server::H3Congestion;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut router = Router::new();
router.route(tako::Method::GET, "/", || async { "h3 tuned" });
let server = Server::builder()
.tls(TlsCert::pem_paths("cert.pem", "key.pem"))
.config(ServerConfig {
h3_congestion: H3Congestion::Bbr,
h3_use_retry: true,
h3_enable_datagrams: true,
..ServerConfig::default()
})
.build();
let handle = server.spawn_h3("[::]:4433", router);
handle.join().await;
Ok(())
}ServerConfig is re-exported as tako::ServerConfig; the H3Congestion enum
lives in tako-rs-server, so import it from there.
The effective per-connection GOAWAY grace at runtime is
min(h3_goaway_grace, drain_timeout) — a long per-connection grace cannot push
the total shutdown past the global drain budget.
h3_goaway_grace larger than drain_timeout is a no-op beyond the global
ceiling. Raise drain_timeout too if you need a longer overall drain window.
Certificate provider note
With http3, rustls is compiled with two crypto providers: aws-lc-rs (its
default) and ring (pulled in by the QUIC stack). Tako installs aws-lc-rs as the
process-wide CryptoProvider when none is installed yet. To use a different one,
install it at startup — for example
rustls::crypto::ring::default_provider().install_default() — before
constructing any server; once a provider is installed, Tako uses it as-is.
Related
- HTTP — HTTP/1.1 and HTTP/2.
- WebTransport — browser WebTransport sessions on
the HTTP/3 server, behind
webtransport. - Feature flags — the
http3feature and what it pulls in.
Last updated on