Skip to content

HTTP/3 Protocol

httpx.zig provides a complete, from-scratch implementation of HTTP/3 (RFC 9114) including QPACK header compression (RFC 9204) and QUIC transport framing (RFC 9000). This guide covers all HTTP/3 features available in the library.

Custom Implementation

Zig's standard library does not provide HTTP/3 or QUIC support. httpx.zig implements these protocols entirely from scratch, following RFC 9114, RFC 9204, and RFC 9000 specifications.

Platform Support

HTTP/3 support works on all platforms:

PlatformArchitectureStatus
Linuxx86_64, aarch64, i386, arm
Windowsx86_64, aarch64, i386, arm
macOSx86_64, aarch64, i386, arm
FreeBSDx86_64, aarch64, i386, arm

Features

  • QPACK Header Compression - Full RFC 9204 implementation with 99-entry static table
  • QUIC Transport Framing - All QUIC frame types (STREAM, CRYPTO, ACK, etc.)
  • Variable-Length Integers - QUIC varint encoding/decoding
  • Connection IDs - Full connection ID management
  • Transport Parameters - QUIC transport parameter encoding

QPACK vs HPACK

QPACK is designed for HTTP/3's out-of-order delivery:

FeatureHPACK (HTTP/2)QPACK (HTTP/3)
Static Table61 entries99 entries
Dynamic TableRequired in-orderAllows out-of-order
BlockingSynchronousAsync with streams
Use CaseTCP (ordered)QUIC (unordered)

QPACK Static Table

zig
const httpx = @import("httpx");

// QPACK has a larger static table
std.debug.print("QPACK static table: {d} entries\n", .{httpx.qpack.StaticTable.entries.len}); // 99
std.debug.print("HPACK static table: {d} entries\n", .{httpx.hpack.StaticTable.entries.len}); // 61

// Common static table lookups
const idx = httpx.qpack.StaticTable.findNameValue(":method", "GET");
if (idx) |index| {
    std.debug.print("Found :method=GET at index {d}\n", .{index});
}

QPACK Encoding

zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

var ctx = httpx.QpackContext.init(allocator);
defer ctx.deinit();

const headers = [_]httpx.qpack.HeaderEntry{
    .{ .name = ":method", .value = "GET" },
    .{ .name = ":path", .value = "/api/v3/resources" },
    .{ .name = ":scheme", .value = "https" },
    .{ .name = ":authority", .value = "api.example.com" },
    .{ .name = "accept", .value = "application/json" },
    .{ .name = "accept-encoding", .value = "gzip, deflate, br" },
};

const encoded = try httpx.qpack.encodeHeaders(&ctx, &headers, allocator);
defer allocator.free(encoded);

std.debug.print("Encoded {d} headers into {d} bytes\n", .{headers.len, encoded.len});

QPACK Encoder Stream

QPACK uses separate streams for encoder/decoder instructions:

zig
var encoder = httpx.qpack.EncoderStream.init();

// Set Dynamic Table Capacity
const cap_instruction = encoder.setDynamicTableCapacity(4096);

// Insert With Name Reference
const insert_instruction = encoder.insertWithNameReference(17, "POST"); // :method=POST

QUIC Packet Structure

Connection IDs

zig
// Create connection IDs
var dcid = httpx.quic.ConnectionId{};
dcid.len = 8;
@memcpy(dcid.data[0..8], &[_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 });

var scid = httpx.quic.ConnectionId{};
scid.len = 4;
@memcpy(scid.data[0..4], &[_]u8{ 0xAA, 0xBB, 0xCC, 0xDD });

Long Header (Initial, Handshake, 0-RTT)

zig
const long_header = httpx.quic.LongHeader{
    .packet_type = .initial,
    .version = .v1,
    .dcid = dcid,
    .scid = scid,
};

var buf: [64]u8 = undefined;
const len = try long_header.encode(&buf);
std.debug.print("Long header: {d} bytes\n", .{len});

// Decode
const decoded = try httpx.quic.LongHeader.decode(&buf);
std.debug.print("Packet type: {s}\n", .{@tagName(decoded.header.packet_type)});

Short Header (1-RTT)

zig
const short_header = httpx.quic.ShortHeader{
    .dcid = dcid,
    .spin_bit = 0,
    .key_phase = 0,
};

var buf: [32]u8 = undefined;
const len = try short_header.encode(&buf);

Packet Types

TypeLong HeaderDescription
InitialConnection establishment
0-RTTEarly data
HandshakeTLS handshake completion
RetryAddress validation
1-RTT❌ (Short)Application data

QUIC Frames

STREAM Frame

Carries application data:

zig
const stream_frame = httpx.quic.StreamFrame{
    .stream_id = 4, // Client-initiated bidirectional stream
    .offset = 0,
    .data = "Hello, HTTP/3!",
    .fin = false,
};

var buf: [128]u8 = undefined;
const len = try stream_frame.encode(&buf);

// Decode
const decoded = try httpx.quic.StreamFrame.decode(buf[0..len]);
std.debug.print("Data: {s}\n", .{decoded.frame.data});

CRYPTO Frame

Carries TLS handshake data:

zig
const crypto_frame = httpx.quic.CryptoFrame{
    .offset = 0,
    .data = &[_]u8{ 0x01, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o' },
};

var buf: [64]u8 = undefined;
const len = try crypto_frame.encode(&buf);

ACK Frame

Acknowledges received packets:

zig
const ack_frame = httpx.quic.AckFrame{
    .largest_acknowledged = 42,
    .ack_delay = 100,
    .first_ack_range = 10,
    .ack_ranges = &.{},
};

var buf: [64]u8 = undefined;
const len = try ack_frame.encode(&buf);

CONNECTION_CLOSE Frame

Terminates a connection:

zig
const close_frame = httpx.quic.ConnectionCloseFrame{
    .error_code = @intFromEnum(httpx.quic.TransportError.no_error),
    .frame_type = null,
    .reason_phrase = "graceful shutdown",
};

var buf: [64]u8 = undefined;
const len = try close_frame.encode(false, &buf); // false = transport close

Frame Types

TypeValueDescription
PADDING0x00Connection-level padding
PING0x01Connectivity check
ACK0x02Acknowledgment
ACK_ECN0x03ACK with ECN counts
RESET_STREAM0x04Abrupt stream termination
STOP_SENDING0x05Request sender stop
CRYPTO0x06TLS handshake data
NEW_TOKEN0x07Address validation token
STREAM0x08-0x0fApplication data
MAX_DATA0x10Connection flow control
MAX_STREAM_DATA0x11Stream flow control
MAX_STREAMS_BIDI0x12Bidirectional stream limit
MAX_STREAMS_UNI0x13Unidirectional stream limit
DATA_BLOCKED0x14Connection blocked
STREAM_DATA_BLOCKED0x15Stream blocked
STREAMS_BLOCKED_BIDI0x16Bidi streams blocked
STREAMS_BLOCKED_UNI0x17Uni streams blocked
NEW_CONNECTION_ID0x18New connection ID
RETIRE_CONNECTION_ID0x19Retire connection ID
PATH_CHALLENGE0x1aPath validation
PATH_RESPONSE0x1bPath validation response
CONNECTION_CLOSE0x1cTransport close
CONNECTION_CLOSE_APP0x1dApplication close
HANDSHAKE_DONE0x1eHandshake complete

Variable-Length Integers

QUIC uses a variable-length integer encoding:

zig
// Encoding
var buf: [8]u8 = undefined;
const len = try httpx.quic.encodeVarInt(15293, &buf);
std.debug.print("Encoded in {d} bytes\n", .{len});

// Decoding
const result = try httpx.quic.decodeVarInt(&buf);
std.debug.print("Value: {d}\n", .{result.value});

Varint Ranges

BytesRange
10 - 63
264 - 16,383
416,384 - 1,073,741,823
81,073,741,824 - 4,611,686,018,427,387,903

HTTP/3 Frame Types

TypeValueDescription
DATA0x00Request/response body
HEADERS0x01QPACK-encoded headers
CANCEL_PUSH0x03Cancel server push
SETTINGS0x04Connection settings
PUSH_PROMISE0x05Server push promise
GOAWAY0x07Connection shutdown
MAX_PUSH_ID0x0dMaximum push ID

HTTP/3 Unidirectional Stream Types

TypeValueDescription
Control0x00Control stream
Push0x01Server push stream
QPACK Encoder0x02QPACK encoder instructions
QPACK Decoder0x03QPACK decoder instructions

Transport Parameters

QUIC transport parameters can be encoded:

zig
const params = httpx.quic.TransportParameters{
    .original_destination_connection_id = null,
    .max_idle_timeout = 30000,
    .max_udp_payload_size = 1350,
    .initial_max_data = 1048576,
    .initial_max_stream_data_bidi_local = 262144,
    .initial_max_stream_data_bidi_remote = 262144,
    .initial_max_stream_data_uni = 262144,
    .initial_max_streams_bidi = 100,
    .initial_max_streams_uni = 100,
};

const encoded = try params.encode(allocator);
defer allocator.free(encoded);

Running the Example

The full HTTP/3 example can be run with:

bash
zig build example-http3_example
./zig-out/bin/http3_example

See Also

Released under the MIT License.