A minimal, sans-I/O HTTP/2 frame codec for WASM and async-free environments.
The Rust ecosystem has excellent HTTP/2 libraries (hyperium/h2), but they all require async runtimes like tokio. This makes them unsuitable for:
- WebAssembly: Browsers don't have tokio
- Embedded systems: No room for async runtimes
- Simple use cases: Don't need full async infrastructure
h2-sans-io gives you a synchronous, zero-dependency frame codec that compiles anywhere Rust does — including wasm32-unknown-unknown.
- Sans-I/O Design: You provide raw bytes, you get parsed events. No sockets, no async, no runtime.
- Pure Rust: Compiles to WASM with no C bindings. Optimized for size (
opt-level = "z", LTO enabled). - RFC 7540 Compliant: Strict validation of all frame types per the HTTP/2 specification:
- DATA — Stream payload with optional padding
- HEADERS — Header block with optional padding and priority
- CONTINUATION — Automatic multi-frame header block assembly
- SETTINGS — Connection parameter negotiation with ACK handling
- RST_STREAM — Stream termination with error codes
- GOAWAY — Graceful connection shutdown
- PING — Connection liveness and RTT measurement
- WINDOW_UPDATE — Flow control window management
- PRIORITY — Stream priority (parsed and validated, not acted on)
- RFC 8441 Support:
SETTINGS_ENABLE_CONNECT_PROTOCOLfor HTTP/2 WebSocket (extended CONNECT with:protocol). - HPACK: Header compression via fluke-hpack with binary-safe headers (no lossy UTF-8 conversion — gRPC binary metadata preserved faithfully).
- Flow Control: WINDOW_UPDATE parsing and frame generation for both connection-level and stream-level windows.
- CONTINUATION Assembly: Automatic header block reassembly across HEADERS + CONTINUATION frames, with a configurable size limit (256 KB) and CONTINUATION interlock enforcement per RFC 7540 §6.10.
- Connection Preface: Automatic h2c (cleartext HTTP/2) preface detection.
- Buffer Protection: Bounded internal buffer (1 MB) prevents memory exhaustion from slow or malicious senders. Size checks run before copying data.
- 302 Unit Tests: Comprehensive test suite covering RFC compliance, edge cases, error recovery, binary HPACK headers, dynamic table state, frame builder roundtrips, padding boundaries, CONTINUATION assembly, and full connection lifecycle simulations.
use h2_sans_io::{H2Codec, H2Event};
let mut codec = H2Codec::new();
let events = codec.process(&frame_bytes).unwrap();
for event in events {
match event {
H2Event::Headers { stream_id, header_block, end_stream } => {
println!("Headers on stream {}", stream_id);
}
H2Event::Data { stream_id, data, end_stream } => {
println!("Data on stream {}: {} bytes", stream_id, data.len());
}
H2Event::Settings { ack, settings } => {
for (id, value) in &settings {
println!("Setting 0x{:x} = {}", id, value);
}
}
H2Event::WindowUpdate { stream_id, increment } => {
println!("Window update: stream {} += {}", stream_id, increment);
}
H2Event::Ping { ack, data } => {
if !ack {
// Echo back with ACK
let pong = H2Codec::create_ping_ack(data);
}
}
H2Event::GoAway { last_stream_id, error_code } => {
println!("GoAway: last_stream={}, error=0x{:x}", last_stream_id, error_code);
}
H2Event::StreamReset { stream_id, error_code } => {
println!("Stream {} reset with error 0x{:x}", stream_id, error_code);
}
}
}use h2_sans_io::H2Codec;
// RST_STREAM (cancel a stream)
let rst = H2Codec::create_rst_stream(stream_id, 0x8); // CANCEL
// WINDOW_UPDATE (connection-level flow control)
let window_update = H2Codec::create_window_update(0, 65535);
// SETTINGS ACK
let settings_ack = H2Codec::create_settings_ack();
// SETTINGS with custom initial window size + extended CONNECT (RFC 8441)
let settings = H2Codec::create_settings_with_window(1 << 20); // 1 MB window
// PING ACK (echo opaque data back)
let pong = H2Codec::create_ping_ack([1, 2, 3, 4, 5, 6, 7, 8]);
// GOAWAY (graceful shutdown)
let goaway = H2Codec::create_goaway(last_stream_id, 0x0); // NO_ERROR
// CONTINUATION (split large header blocks)
let cont = H2Codec::create_continuation_frame(stream_id, &header_block_fragment, true);
// HEADERS frame (single frame, sets END_HEADERS)
let headers = H2Codec::create_headers_frame(stream_id, &hpack_block, end_stream);
// HEADERS + CONTINUATION frames (auto-splits if block > max_frame_size)
let frames = H2Codec::create_headers_frames(stream_id, &hpack_block, end_stream, 16384);
// DATA frames (auto-splits if data > max_frame_size, END_STREAM on last)
let frames = H2Codec::create_data_frames(stream_id, &body, true, 16384);Header names and values are Vec<u8> to preserve binary content faithfully (e.g. gRPC -bin metadata suffixed headers). Use name_str() / value_str() for UTF-8 access.
use h2_sans_io::{HpackDecoder, HpackEncoder, H2Header};
// Encode headers
let mut encoder = HpackEncoder::new();
let headers = vec![
H2Header::new(":method", "GET"),
H2Header::new(":path", "/api/data"),
H2Header::new("x-custom-bin", vec![0x00, 0xFF, 0x80]), // binary value
];
let encoded = encoder.encode(&headers);
// Decode headers
let mut decoder = HpackDecoder::new();
let decoded = decoder.decode(&encoded).unwrap();
assert_eq!(decoded[0].name_str().unwrap(), ":method");
assert_eq!(decoded[0].value_str().unwrap(), "GET");
// Binary values are preserved without lossy conversion
assert_eq!(decoded[2].value, vec![0x00, 0xFF, 0x80]);The error_code module provides all RFC 7540 §7 error codes as constants:
use h2_sans_io::error_code;
let rst = H2Codec::create_rst_stream(1, error_code::CANCEL);
let goaway = H2Codec::create_goaway(0, error_code::NO_ERROR);Available codes: NO_ERROR, PROTOCOL_ERROR, INTERNAL_ERROR, FLOW_CONTROL_ERROR, SETTINGS_TIMEOUT, STREAM_CLOSED, FRAME_SIZE_ERROR, REFUSED_STREAM, CANCEL, COMPRESSION_ERROR, CONNECT_ERROR, ENHANCE_YOUR_CALM, INADEQUATE_SECURITY, HTTP_1_1_REQUIRED.
This crate is intentionally minimal. It provides:
- Frame parsing — raw bytes in, typed
H2Eventvariants out - Frame encoding — static builders for all control frame types
- HPACK wrapper — stateful header compression/decompression
- Incremental parsing — handles partial frames across multiple
process()calls
It does not provide:
- TCP/TLS transport (you provide the bytes)
- Connection/stream lifecycle management
- Priority scheduling or dependency trees
- Server push (PUSH_PROMISE frames are silently ignored)
- Browser-based MITM proxies: WASM kernels intercepting HTTP/2 traffic
- WASM HTTP/2 clients: Client-side HTTP/2 without server infrastructure
- Embedded HTTP/2 servers: Resource-constrained environments
- Protocol testing: Test HTTP/2 implementations without async complexity
- Educational: Learn how HTTP/2 framing works at the wire level
| Crate | Async | WASM | Scope |
|---|---|---|---|
| hyperium/h2 | tokio | No | Full client/server |
| h2-sans-io | None | Yes | Frame codec + HPACK |
- Rust 2021 edition (1.56+)
- Single dependency: fluke-hpack 0.3
MIT