Skip to content

ariel42/h2-sans-io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

h2-sans-io

A minimal, sans-I/O HTTP/2 frame codec for WASM and async-free environments.

Why?

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.

Features

  • 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_PROTOCOL for 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.

Quick Start

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);
        }
    }
}

Encoding Frames

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);

HPACK (Header Compression)

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]);

Error Codes

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.

Architecture

This crate is intentionally minimal. It provides:

  • Frame parsing — raw bytes in, typed H2Event variants 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)

Use Cases

  • 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

Comparison

Crate Async WASM Scope
hyperium/h2 tokio No Full client/server
h2-sans-io None Yes Frame codec + HPACK

Requirements

  • Rust 2021 edition (1.56+)
  • Single dependency: fluke-hpack 0.3

License

MIT

About

A minimal, sans-I/O HTTP/2 frame codec for WASM and async-free environments

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages