WebTransport Support¶
This guide covers WebTransport support in Quill, enabling bidirectional communication over HTTP/3 with support for streams and unreliable datagrams.
Table of Contents¶
- Overview
- Enabling WebTransport
- Server Setup
- Client Setup
- Browser Integration
- Sessions
- Streams
- Datagrams
- Use Cases
- Configuration
- Error Handling
- Best Practices
Overview¶
WebTransport is a protocol for bidirectional communication between a browser and server, built on HTTP/3. It provides:
- Bidirectional Streams: Full-duplex communication channels
- Unidirectional Streams: One-way communication channels
- Unreliable Datagrams: Low-latency, unordered message delivery
- Browser Native Support: Direct WebTransport API in modern browsers
- Multiplexing: Multiple streams over a single connection
Why WebTransport?¶
WebTransport is ideal for: - Real-Time Applications: Gaming, collaborative editing, live streaming - Browser Games: Low-latency bidirectional communication - IoT Dashboards: Streaming sensor data to browsers - Video Conferencing: Media streaming with unreliable datagrams - Live Updates: Push-based real-time notifications
WebTransport vs WebSockets¶
| Feature | WebTransport | WebSockets |
|---|---|---|
| Protocol | HTTP/3 (QUIC) | HTTP/1.1 |
| Streams | Multiple concurrent | Single stream |
| Unreliable delivery | Yes (datagrams) | No |
| Head-of-line blocking | No | Yes |
| Connection migration | Yes | No |
Enabling WebTransport¶
Enable the webtransport feature in your Cargo.toml:
Dependencies¶
The WebTransport implementation uses: - h3-webtransport: WebTransport over HTTP/3 (v0.1) - h3-datagram: HTTP/3 datagram support (v0.0.2) - quinn: QUIC implementation (via http3 feature) - rustls: TLS 1.3 encryption
Server Setup¶
Basic WebTransport Server¶
use quill_transport::{
WebTransportServerBuilder, WebTransportServer, Session,
FnWebTransportHandler, WebTransportError,
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> Result<(), WebTransportError> {
let addr: SocketAddr = "0.0.0.0:4433".parse().unwrap();
// Build server with configuration
let server = WebTransportServerBuilder::new(addr)
.enable_datagrams(true)
.max_datagram_size(65536)
.max_sessions(100)
.max_concurrent_streams(100)
.session_timeout_ms(60000)
.build()?;
// Create a session handler
let handler = FnWebTransportHandler::new(|session: Session| async move {
println!(
"New session: id={}, remote={}",
session.session_id(),
session.remote_addr()
);
// Handle session...
Ok(())
});
println!("WebTransport server listening on {}", server.bind_addr());
// Start serving (full implementation pending)
// server.serve(handler).await?;
Ok(())
}
Server Builder Options¶
let server = WebTransportServerBuilder::new(addr)
// Enable HTTP/3 datagrams
.enable_datagrams(true)
// Maximum datagram payload size
.max_datagram_size(65536)
// Maximum concurrent sessions
.max_sessions(100)
// Maximum concurrent streams per session
.max_concurrent_streams(100)
// Session idle timeout in milliseconds
.session_timeout_ms(60000)
// HTTP/3 idle timeout
.idle_timeout_ms(120000)
// HTTP/3 keep-alive interval
.keep_alive_interval_ms(30000)
.build()?;
Custom Session Handler¶
Implement the WebTransportHandler trait for more complex logic:
use quill_transport::{WebTransportHandler, Session, WebTransportError};
use std::future::Future;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
struct GameSessionHandler {
active_games: Arc<Mutex<HashMap<u64, GameState>>>,
}
impl WebTransportHandler for GameSessionHandler {
type Future = impl Future<Output = Result<(), WebTransportError>> + Send;
fn handle(&self, session: Session) -> Self::Future {
let games = self.active_games.clone();
async move {
// Create a new game state for this session
let game_state = GameState::new();
games.lock().await.insert(session.session_id(), game_state);
// Handle game logic...
// Clean up on disconnect
games.lock().await.remove(&session.session_id());
Ok(())
}
}
}
Client Setup¶
Native Rust Client¶
use quill_transport::{WebTransportClientBuilder, WebTransportError};
use bytes::Bytes;
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> Result<(), WebTransportError> {
// Install crypto provider
let _ = rustls::crypto::ring::default_provider().install_default();
// Build client
let client = WebTransportClientBuilder::new()
.enable_datagrams(true)
.max_datagram_size(65536)
.build()?;
// Connect to server
let addr: SocketAddr = "127.0.0.1:4433".parse().unwrap();
let session = client.connect(addr, "localhost", "/webtransport").await?;
// Send a datagram
session.send_datagram(Bytes::from("Hello!"))?;
// Open a bidirectional stream
let (mut send, mut recv) = session.open_bi().await?;
// Use send/recv for streaming data...
// Open a unidirectional stream
let mut send_uni = session.open_uni().await?;
// Use send_uni for one-way data...
// Accept incoming streams
if let Some((send, recv)) = session.accept_bi().await? {
// Handle incoming bidirectional stream
}
Ok(())
}
Client Builder Options¶
let client = WebTransportClientBuilder::new()
// Enable HTTP/3 datagrams
.enable_datagrams(true)
// Maximum datagram payload size
.max_datagram_size(65536)
// Enable 0-RTT for fast resumption (idempotent only)
.enable_zero_rtt(false)
// Connection idle timeout
.idle_timeout_ms(60000)
// Keep-alive interval
.keep_alive_interval_ms(30000)
.build()?;
Browser Integration¶
JavaScript WebTransport API¶
Modern browsers support WebTransport natively:
// Connect to WebTransport server
const transport = new WebTransport('https://localhost:4433/webtransport');
await transport.ready;
console.log('Connected!');
// Handle connection close
transport.closed.then(() => {
console.log('Connection closed gracefully');
}).catch(error => {
console.error('Connection closed with error:', error);
});
Sending Datagrams¶
// Send a datagram
const writer = transport.datagrams.writable.getWriter();
await writer.write(new TextEncoder().encode('Hello from browser!'));
writer.releaseLock();
// Read datagrams
const reader = transport.datagrams.readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log('Received:', new TextDecoder().decode(value));
}
Using Bidirectional Streams¶
// Create a bidirectional stream
const stream = await transport.createBidirectionalStream();
// Write to stream
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode('Stream message'));
writer.releaseLock();
// Read from stream
const reader = stream.readable.getReader();
const { value } = await reader.read();
console.log('Response:', new TextDecoder().decode(value));
reader.releaseLock();
Using Unidirectional Streams¶
// Create outgoing unidirectional stream
const outgoingStream = await transport.createUnidirectionalStream();
const writer = outgoingStream.getWriter();
await writer.write(new TextEncoder().encode('One-way message'));
await writer.close();
// Accept incoming unidirectional streams
const reader = transport.incomingUnidirectionalStreams.getReader();
while (true) {
const { value: stream, done } = await reader.read();
if (done) break;
// Read from incoming stream
const streamReader = stream.getReader();
const { value } = await streamReader.read();
console.log('Incoming:', new TextDecoder().decode(value));
}
Sessions¶
Session Object¶
The Session represents an active WebTransport connection:
use quill_transport::Session;
fn handle_session(session: Session) {
// Get session ID (unique per connection)
let id = session.session_id();
// Get remote address
let addr = session.remote_addr();
// Get session configuration
let config = session.config();
println!("Max datagram size: {}", config.max_datagram_size);
}
Client Session¶
The ClientSession provides client-side session operations:
use quill_transport::ClientSession;
use bytes::Bytes;
async fn use_session(session: ClientSession) -> Result<(), WebTransportError> {
// Send datagram
session.send_datagram(Bytes::from("data"))?;
// Receive datagram
if let Some(data) = session.recv_datagram().await? {
println!("Received: {:?}", data);
}
// Open bidirectional stream
let (send, recv) = session.open_bi().await?;
// Open unidirectional stream
let send = session.open_uni().await?;
// Accept incoming bidirectional stream
if let Some((send, recv)) = session.accept_bi().await? {
// Handle incoming stream
}
// Accept incoming unidirectional stream
if let Some(recv) = session.accept_uni().await? {
// Handle incoming stream
}
Ok(())
}
Streams¶
Bidirectional Streams (BiStream)¶
Full-duplex communication channels:
use quill_transport::BiStream;
use bytes::Bytes;
async fn handle_bi_stream(stream: BiStream) -> Result<(), WebTransportError> {
let (mut send, mut recv) = stream;
// Read data
let mut buf = vec![0u8; 1024];
let n = recv.read(&mut buf).await?;
// Write response
send.write_all(&buf[..n]).await?;
send.finish().await?;
Ok(())
}
Unidirectional Streams (UniStream)¶
One-way communication channels:
use quill_transport::UniStream;
// Send-only stream (client opens, server receives)
async fn send_data(mut stream: UniStream) -> Result<(), WebTransportError> {
stream.write_all(b"Hello, server!").await?;
stream.finish().await?;
Ok(())
}
// Receive-only stream (server accepts)
async fn recv_data(mut stream: RecvStream) -> Result<Vec<u8>, WebTransportError> {
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await?;
Ok(buf)
}
Datagrams¶
Sending Datagrams¶
use bytes::Bytes;
// Simple datagram
session.send_datagram(Bytes::from("sensor:temp=72.5"))?;
// Binary datagram
let data = [0x01, 0x02, 0x03, 0x04];
session.send_datagram(Bytes::copy_from_slice(&data))?;
Receiving Datagrams¶
// Receive single datagram
if let Some(data) = session.recv_datagram().await? {
println!("Received {} bytes", data.len());
}
// Receive datagrams in a loop
loop {
match session.recv_datagram().await? {
Some(data) => process_datagram(data),
None => break, // Connection closed
}
}
Datagram Best Practices¶
- Size: Keep datagrams < 1200 bytes to avoid fragmentation
- Loss: Design for datagram loss (no delivery guarantee)
- Ordering: Handle out-of-order delivery
- Rate: Implement application-level flow control
- FEC: Consider Forward Error Correction for important data
Use Cases¶
Real-Time Game Example¶
use quill_transport::{
WebTransportServerBuilder, FnWebTransportHandler, Session,
};
use bytes::Bytes;
// Game update packet
#[derive(Debug)]
struct GameUpdate {
player_id: u64,
x: f32,
y: f32,
action: u8,
}
impl GameUpdate {
fn encode(&self) -> Bytes {
let mut buf = Vec::with_capacity(25);
buf.extend_from_slice(&self.player_id.to_le_bytes());
buf.extend_from_slice(&self.x.to_le_bytes());
buf.extend_from_slice(&self.y.to_le_bytes());
buf.push(self.action);
Bytes::from(buf)
}
fn decode(data: &[u8]) -> Option<Self> {
if data.len() < 21 { return None; }
Some(GameUpdate {
player_id: u64::from_le_bytes(data[0..8].try_into().ok()?),
x: f32::from_le_bytes(data[8..12].try_into().ok()?),
y: f32::from_le_bytes(data[12..16].try_into().ok()?),
action: data[20],
})
}
}
// Server handler
let handler = FnWebTransportHandler::new(|session: Session| async move {
println!("Player connected: {}", session.session_id());
// Use datagrams for position updates (tolerate loss)
// Use streams for chat messages (reliable)
Ok(())
});
IoT Sensor Dashboard¶
// Sensor reading
struct SensorReading {
sensor_id: u32,
timestamp: u64,
value: f64,
unit: &'static str,
}
// Browser connects and receives sensor updates via datagrams
let handler = FnWebTransportHandler::new(|session: Session| async move {
let session_id = session.session_id();
// Stream sensor data to browser
loop {
let reading = get_sensor_reading().await;
// Send via datagram (low latency, tolerate loss)
if let Err(e) = session.send_datagram(reading.encode()) {
tracing::warn!("Failed to send reading: {}", e);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
Ok(())
});
Chat Application¶
// Chat message
enum ChatMessage {
Text(String),
Typing { user_id: u64 },
Read { message_id: u64 },
}
let handler = FnWebTransportHandler::new(|session: Session| async move {
// Use bidirectional stream for reliable chat messages
let (mut send, mut recv) = session.open_bi().await?;
// Use datagrams for ephemeral updates (typing indicators)
loop {
tokio::select! {
// Receive chat message (reliable stream)
msg = recv_message(&mut recv) => {
broadcast_message(msg).await;
}
// Send typing indicator (unreliable datagram)
_ = typing_indicator_timer.tick() => {
let _ = session.send_datagram(typing_packet());
}
}
}
Ok(())
});
Configuration¶
WebTransportConfig¶
use quill_transport::WebTransportConfig;
let config = WebTransportConfig {
// HTTP/3 base configuration
http3: HyperConfig {
enable_zero_rtt: false,
enable_datagrams: true,
enable_connection_migration: true,
max_concurrent_streams: 100,
max_datagram_size: 65536,
keep_alive_interval_ms: 30000,
idle_timeout_ms: 60000,
},
// WebTransport-specific settings
max_sessions: 1000, // Maximum concurrent sessions
session_timeout_ms: 300000, // 5-minute session timeout
enable_datagrams: true, // Enable datagram support
max_datagram_size: 65536, // Maximum datagram size
};
Configuration Best Practices¶
| Setting | Development | Production |
|---|---|---|
max_sessions |
100 | 10,000+ |
session_timeout_ms |
60,000 | 300,000 |
max_datagram_size |
65,536 | 1,200 |
max_concurrent_streams |
100 | 1,000 |
idle_timeout_ms |
30,000 | 120,000 |
Error Handling¶
WebTransportError¶
use quill_transport::WebTransportError;
fn handle_error(error: WebTransportError) {
match error {
WebTransportError::ConnectionError(msg) => {
eprintln!("Connection failed: {}", msg);
}
WebTransportError::SessionError(msg) => {
eprintln!("Session error: {}", msg);
}
WebTransportError::StreamError(msg) => {
eprintln!("Stream error: {}", msg);
}
WebTransportError::DatagramError(msg) => {
eprintln!("Datagram error: {}", msg);
}
WebTransportError::ConfigError(msg) => {
eprintln!("Configuration error: {}", msg);
}
WebTransportError::TlsError(msg) => {
eprintln!("TLS error: {}", msg);
}
}
}
Graceful Shutdown¶
// Server shutdown
async fn shutdown_server(server: WebTransportServer) {
// Signal shutdown to all sessions
server.shutdown().await;
}
// Client session close
async fn close_session(session: ClientSession) {
// Close all streams gracefully
session.close(0, b"goodbye").await;
}
Best Practices¶
1. Choose the Right Transport¶
- Streams: Reliable, ordered data (chat messages, file transfers)
- Datagrams: Real-time, loss-tolerant data (game state, sensor readings)
2. Handle Connection Lifecycle¶
let handler = FnWebTransportHandler::new(|session: Session| async move {
// 1. Initialize session state
let state = SessionState::new(session.session_id());
// 2. Main session loop
loop {
tokio::select! {
result = session.recv_datagram() => {
match result {
Ok(Some(data)) => state.process_datagram(data),
Ok(None) => break, // Connection closed
Err(e) => {
tracing::error!("Error: {}", e);
break;
}
}
}
// Handle other events...
}
}
// 3. Clean up session state
state.cleanup();
Ok(())
});
3. Implement Backpressure¶
// Track outstanding datagrams
let outstanding = Arc::new(AtomicU32::new(0));
// Send with backpressure
if outstanding.load(Ordering::Relaxed) < MAX_OUTSTANDING {
session.send_datagram(data)?;
outstanding.fetch_add(1, Ordering::Relaxed);
} else {
tracing::warn!("Dropping datagram due to backpressure");
}
4. Use Appropriate Timeouts¶
use tokio::time::{timeout, Duration};
// Timeout for stream operations
let result = timeout(
Duration::from_secs(5),
session.open_bi()
).await??;
// Timeout for datagram receive
match timeout(Duration::from_millis(100), session.recv_datagram()).await {
Ok(Ok(Some(data))) => process(data),
Ok(Ok(None)) => return Ok(()), // Closed
Ok(Err(e)) => return Err(e),
Err(_) => {} // Timeout, continue
}
5. Monitor Session Health¶
// Send periodic pings
let ping_interval = tokio::time::interval(Duration::from_secs(10));
loop {
tokio::select! {
_ = ping_interval.tick() => {
let ping = Message::Ping(sequence.fetch_add(1, Ordering::Relaxed));
if session.send_datagram(ping.encode()).is_err() {
break; // Connection lost
}
}
// Handle other events...
}
}
Browser Compatibility¶
| Browser | WebTransport Support |
|---|---|
| Chrome | 97+ (stable) |
| Edge | 97+ (stable) |
| Firefox | 114+ (enabled by default) |
| Safari | Not yet supported |
| Opera | 83+ (stable) |
Feature Detection¶
if (typeof WebTransport === 'undefined') {
console.log('WebTransport not supported, falling back to WebSocket');
// Use WebSocket fallback
} else {
// Use WebTransport
const transport = new WebTransport(url);
}
See Also¶
- HTTP/3 Configuration - Hyper profile documentation
- Performance Guide - Performance optimization
- Examples - WebTransport example code