// Complete HPACK (RFC 7541): the header-compression stack a gRPC connection runs on.
// The 61-entry static table, prefix integers (§5.1), string literals with Huffman
// coding (§5.2, Appendix B), and the size-bounded dynamic table with eviction (§4).
// This drives each primitive on its own, then the stateful encoder/decoder pair over
// a header list, printing the compressed bytes and the exact round-trip.
//
// moon run src/examples/04-hpack
///|
#coverage.skip
fn main {
run() catch {
e => println("error: \{e}")
}
}
///|
fn run() -> Unit raise {
// The static table indexes the header fields every request repeats. Index 2 is the
// `:method GET` entry; index 8 is `:status 200`.
let table = @hpack.static_table
println("static table: \{table.length()} entries")
match @hpack.entry(2) {
Some(h) => println(" [2] = \{h.ascii().0}: \{h.ascii().1}")
None => ()
}
match @hpack.entry(8) {
Some(h) => println(" [8] = \{h.ascii().0}: \{h.ascii().1}")
None => ()
}
// Prefix integers (§5.1): a value below the prefix max fits one octet; 1337 with a
// 5-bit prefix spills into continuation octets. The arithmetic is `moonvar`'s, since
// QPACK counts in the same integer and neither of them owns it.
for value in [10, 31, 1337, 66666] {
let enc = @prefix.encode(value, prefix=5)
let status = try {
let (back, consumed) = @prefix.decode(enc[:], prefix=5)
if back == value && consumed == enc.length() {
"ok"
} else {
"MISMATCH"
}
} catch {
e => "raised \{e}"
}
println(
" int \{value} (5-bit prefix) = \{@base16.encode(enc)} (\{status})",
)
}
// String literals (§5.2): the raw form and the Huffman form of the same octets. The
// `auto` encoder keeps whichever is shorter, setting the `H` bit when it Huffman-codes.
let host = b"www.example.com"
let raw = @hpack.string(host[:], huffman=false)
let huff = @hpack.string(host[:])
let auto = @hpack.string(host[:])
println("string \"www.example.com\":")
println(" raw \{raw.length()} octets, H=\{@prefix.flagged(raw[:])}")
println(
" huffman \{huff.length()} octets, H=\{@prefix.flagged(huff[:])} (§C.4.1: \{@base16.encode(@huffman.encode(host))})",
)
println(" auto picks the huffman form: \{auto == huff}")
let (read_back, _) = @hpack.read_string(auto, 0)
println(" decoded back: \{read_back == host}")
// The dynamic table (§4): a FIFO of recently seen fields, each costing
// `name + value + 32` octets, bounded by its max size. Filling past the bound evicts
// the oldest entry; shrinking the bound evicts to fit.
let tbl = @hpack.Table::new(limit=100)
tbl.add(b"custom-key", b"heke1228")
tbl.add(b"x-trace-id", b"23333")
println("dynamic table: \{tbl.count()} entries, \{tbl.size()}/100 octets")
tbl.add(b"another-header", b"a fairly long value that forces eviction")
println(
" after a large insert: \{tbl.count()} entries, \{tbl.size()}/100 octets",
)
tbl.resize(40)
println(
" after shrinking to 40: \{tbl.count()} entries, \{tbl.size()}/40 octets",
)
println(" size-update opcode: \{@base16.encode(@hpack.size_update(40))}")
// The stateful encoder/decoder pair over a real header list — the block a client puts
// in a HEADERS frame, decoded back to the same fields.
let enc = @hpack.Encoder::new(huffman=true)
let headers : Array[@header.Header] = [
{ name: b":method", value: b"POST", },
{ name: b":path", value: b"/greet.Greeter/SayHello", },
{ name: b":authority", value: b"www.example.com", },
{ name: b"content-type", value: b"application/grpc+proto", },
]
let block = enc.encode(headers)
let dec = @hpack.Decoder::new()
let decoded = dec.decode(block)
println(
"header block: \{headers.length()} fields -> \{block.length()} octets -> \{decoded.length()} fields, round-trip \{decoded == headers}",
)
}