flare_mlake · API · qpack

flare.qpack.codec

QPACK encoder + decoder (RFC 9204) -- static-only.

QPACK is the HPACK-shaped header compression layer that HTTP/3 uses on top of QUIC. The full RFC 9204 codec ships a dynamic table that streams insertions on a dedicated unidirectional QUIC stream (the "encoder stream"), with field sections referencing entries in the dynamic table by index. The dynamic table introduces head-of-line blocking risk on the QUIC layer (field sections may need to wait for inserts they reference) and a non-trivial encoder-stream protocol on top of the simple field-section codec.

This module is the static-only subset:

  • Field section prefix: required-insert-count and base both fixed at 0 on encode; decoder rejects field sections whose required-insert-count is not 0 (no dynamic-table support yet).
  • Field lines: indexed-static, literal-with-static-name, and literal-with-literal-name. The dynamic-reference variants (1Tppppppp with T=0, plus 001Nhppp post-base) are rejected on decode.
  • String literals: optional Huffman per RFC 7541 Appendix B (shared with HPACK; the encoder picks the shorter form).

The static-only subset is enough for an HTTP/3 server that talks to a real client: the most common request and response shapes (:method GET, :status 200, common content types) all hit the static table directly, and literal field lines cover everything else without head-of-line risk.

Public surface:

  • :class:QpackHeader -- (name, value) pair, re-exported from flare.http2.hpack for ergonomic interop.
  • :func:encode_field_section -- emit the field section prefix plus all field lines into a fresh byte list.
  • :func:decode_field_section -- parse a field section into a list of :class:QpackHeader. Rejects dynamic-table references.
  • :data:QPACK_STATIC_TABLE_SIZE -- the 99 of the static table.

Sans-I/O contract: zero I/O imports; registered in tests/tools/check_sans_io.sh.

References:

  • RFC 9204 "QPACK: Field Compression for HTTP/3".
  • RFC 9114 §4.2 "HTTP Fields" (lowercase mandate).
  • RFC 7541 Appendix B (Huffman -- shared with HPACK).

Functions

encode_field_section

def encode_field_section(headers: List[HpackHeader], mut out: List[UInt8])

Append the QPACK-encoded field section bytes (RFC 9204 §4.5) to out.

The caller owns the buffer and may pass the same List[UInt8] through repeated encode calls, clearing between calls so the underlying allocation amortises across the stream. The encoder appends only; it never reads from or truncates the existing contents.

The static-only codec emits:

  • Field section prefix: required_insert_count=0, sign=0 + delta_base=0 (two single-byte 0x00s).
  • Per header, the lowest-cost wire shape:
    • full match in the static table -> indexed field line (11pppppp for indices < 63, with continuation bytes for higher indices; the static-table flag T is always 1).
    • name-only match -> literal-with-name-reference (0101pppp with T=1 for static, then string literal).
    • no match -> literal-with-literal-name (0010nnnn with H+name length, then name, then value).

Header names must be lowercase per RFC 9114 §4.2; the encoder emits whatever case the caller provides and trusts the call site. The decoder mirrors this -- it surfaces the bytes verbatim.

encode_field_line

def encode_field_line(name: String, value: String, mut out: List[UInt8])

Append one static-only field line (RFC 9204 §4.5) for name: value to out: indexed when the pair is in the static table, a name reference when only the name is, otherwise a literal name. Follows the two-byte prefix :func:encode_field_section writes; exposed so an encoder can stream fields without building a header list first.

decode_field_section

def decode_field_section(buf: Span[UInt8], max_field_section_size: UInt64 = UInt64(65536)) -> List[HpackHeader]

Parse a QPACK field section.

Static-only: rejects field sections whose required-insert-count is not 0 (the encoder never emits one above 0 either; a peer that sends a non-zero count is using the dynamic table we don't support yet, and the right behaviour is to surface a connection error rather than mis- interpret the payload).

Returns the parsed headers in wire order. Field-line type dispatch:

  • 1Txxxxxx -- indexed; rejects T=0 (dynamic; not supported).
  • 01NTxxxx -- literal with name reference; rejects T=0.
  • 001Nhxxx -- literal with literal name.
  • 0001xxxx -- indexed post-base (dynamic only) -> reject.
  • 0000Nhxx -- literal with name reference post-base (dynamic only) -> reject.

The decoded size (name + value + 32 per line) is capped at max_field_section_size; the section is rejected as soon as a line would pass it.

Aliases

comptime QPACK_DEFAULT_MAX_FIELD_SECTION_SIZE = UInt64(65536)

Default cap on a decoded field section: the sum of name + value + 32 over its field lines, the size RFC 9114 section 4.2.2 gives SETTINGS_MAX_FIELD_SECTION_SIZE. Matches the 64 KiB flare advertises.

comptime QPACK_FIELD_SECTION_TOO_LARGE = "qpack: field section above limit"

Error text raised when a decoded section passes the cap. Callers match on field section above limit.