Skip to main content

QPACK — RFC 9204

QPACK is the HPACK variant adapted for HTTP/3's out-of-order stream delivery. Unlike HPACK's in-stream dynamic table updates, QPACK isolates table mutations onto separate encoder and decoder streams so field sections on request streams can reference the table without creating head-of-line blocking.

core.net.h3.qpack modularity:

ModuleRole
qpack.static_tableRFC 9204 Appendix A — the 99 static entries
qpack.dynamic_tableSize-bounded LRU insert window
qpack.integerPrefixed-integer codec (§4.1.1)
qpack.huffmanHuffman encoder / decoder (RFC 7541 Appendix B)
qpack.encoderField-section emitter
qpack.decoderField-section parser + HeaderField
qpack.instructionsEncoder + decoder stream opcodes
qpack.sessionWrapper tying encoder/decoder to their streams

Prefixed integers (§4.1.1)

Integers are encoded with a variable-length prefix on the high bits of the first byte plus continuation bytes with top bit set:

if value < 2^N - 1:
emit [opcode | value] ← N-bit-prefix + opcode
else:
emit [opcode | (2^N - 1)] ← mask overflow
remaining = value - (2^N - 1)
while remaining >= 128:
emit [(remaining % 128) | 0x80] ← continuation bit set
remaining /= 128
emit [remaining] ← final byte, top bit clear

Examples (5-bit prefix with opcode 0x00):

ValueWire
100x0A
310x1F 0x00 (inline cap then 0 continuation)
320x1F 0x01
13370xDF 0x9A 0x0A (with opcode 0xC0, 5-bit prefix)

See qpack_integer_kat.

Static table (§3.2.2)

99 entries fixed at spec time. Key rows:

IndexNameValue
0:authority``
1:path/
17:methodGET
21:methodOPTIONS
23:schemehttps
25:status200
29:status500
79user-agent``

Full 99 rows in core.net.h3.qpack.static_table.STATIC_TABLE — exhaustive pin in qpack_static_table_rfc9204.

Dynamic table (§3.2.1)

Size-bounded by the QPACK_MAX_TABLE_CAPACITY SETTING the peer advertised. Insertions via the encoder stream; each entry has an absolute index assigned at insert time.

public type DynamicTable is { /* ring-buffer + size accounting */ };

public fn DynamicTable.new(max_capacity: UInt64) -> DynamicTable;
public fn DynamicTable.insert(&mut self, name: Text, value: Text)
-> Result<Int, QpackError>; ← returns absolute index
public fn DynamicTable.get(&self, abs_index: Int) -> Maybe<Entry>;
public fn DynamicTable.set_max_capacity(&mut self, new_cap: UInt64);

Entries evict from the oldest end when current_size > max_capacity. current_size = Σ (len(name) + len(value) + 32) .1.

See qpack_dynamic_table.

Encoder-stream instructions (§4.3.1)

Opcode taxonomy (high bits of first byte):

OpcodeInstructionPrefix width
001Set Dynamic Table Capacity5 bits
1TInsert with Name Reference (T = static flag)6 bits
01HInsert with Literal Name (H = Huffman flag)5 bits
000Duplicate (ref existing entry)5 bits

Opcode byte layout:

Set Capacity: 0 0 1 x x x x x
Insert w/ Name Ref: 1 T x x x x x x (T=1 → static, T=0 → dynamic)
Insert w/ Literal Name: 0 1 H x x x x x
Duplicate: 0 0 0 x x x x x

See qpack_instructions_kat.

Decoder-stream instructions (§4.3.2)

OpcodeInstructionPrefix width
1Section Acknowledgement7 bits
01Stream Cancellation6 bits
00Insert Count Increment6 bits
Section Ack: 1 x x x x x x x (stream_id)
Stream Cancel: 0 1 x x x x x x (stream_id)
Insert Count Increment: 0 0 x x x x x x (delta)

Field sections (§4.5)

A field section is the payload of a HEADERS or PUSH_PROMISE frame. Prefix:

[Encoded Required Insert Count (8-bit prefix, varint)]
[Sign-bit + Delta Base (7-bit prefix, varint)]
[field lines...]

For the QPACK_MAX_TABLE_CAPACITY = 0 profile warp defaults to (dynamic table disabled), both prefixes are 0 — wire prefix is 0x00 0x00.

Per-line opcodes:

OpcodeLinePrefix
11TIndexed field line (T = static)6 bits
0101TLiteral field line with name ref (static only in this profile)4 bits
0010NHLiteral field line with literal name3 bits

Example — :method: GET (static index 17):

[0x00 0x00] ← RIC + Delta Base
[0xD1] ← 0xC0 | 17 — indexed field line, static

Example — three-field request line for GET https://example.com/:

[0x00 0x00]
[0xD1] ← :method GET (static 17)
[0xD7] ← :scheme https (static 23)
[0xC1] ← :path / (static 1)

See qpack_field_section_kat.

Huffman codec (RFC 7541 Appendix B)

QPACK reuses the HPACK Huffman table. Strings inside field lines carry a leading bit indicating Huffman-vs-literal encoding:

[H x x x x x x x] ← H = Huffman flag, 7-bit prefix for length
[encoded bytes...]

Encoder chooses Huffman iff the encoded length is strictly shorter than the raw byte length. See qpack_huffman_roundtrip and rfc9204_qpack_huffman.

API surface

Symmetric client + server consumption:

mount core.net.h3.qpack.{HeaderField, encode_field_section, decode_field_section};

let headers: List<HeaderField> = [
HeaderField { name: f":status", value: f"200" },
HeaderField { name: f"content-type", value: f"application/json" },
];
let wire: List<Byte> = encode_field_section(&headers);
// … send as a HEADERS frame payload …

let parsed = decode_field_section(wire.as_slice()).unwrap();
assert(parsed.len() == 2);

References

  • vcs/specs/L2-standard/net/h3/qpack_integer_kat.vr
  • vcs/specs/L2-standard/net/h3/qpack_static_table_rfc9204.vr
  • vcs/specs/L2-standard/net/h3/qpack_static_table_coverage.vr
  • vcs/specs/L2-standard/net/h3/qpack_dynamic_table.vr
  • vcs/specs/L2-standard/net/h3/qpack_dynamic_table_surface.vr
  • vcs/specs/L2-standard/net/h3/qpack_encode_decode_roundtrip.vr
  • vcs/specs/L2-standard/net/h3/qpack_encoder_decoder_roundtrip.vr
  • vcs/specs/L2-standard/net/h3/qpack_huffman_roundtrip.vr
  • vcs/specs/L2-standard/net/h3/qpack_field_section_kat.vr
  • vcs/specs/L2-standard/net/h3/qpack_instructions_kat.vr
  • vcs/specs/L2-standard/net/h3/qpack_session_typecheck.vr
  • vcs/specs/L2-standard/net/h3/qpack_error_variants.vr
  • vcs/specs/L2-standard/net/kat/rfc9204_qpack_huffman.vr