Internals

View Source

Architecture

erlang_qpack consists of two main modules:

  • qpack - Main QPACK encoder/decoder implementing RFC 9204
  • qpack_huffman - Huffman coding implementation per RFC 7541 Appendix B

State Record

The internal state record tracks all encoding/decoding context:

-record(qpack, {
    use_dynamic,           %% boolean() - dynamic table enabled
    dyn_field_index,       %% #{header() => AbsIndex} - O(1) exact match
    dyn_name_index,        %% #{binary() => AbsIndex} - O(1) name match
    dyn_entries,           %% [{AbsIndex, Header, Size}] - FIFO list
    dyn_size,              %% Current dynamic table size in bytes
    dyn_max_size,          %% Maximum dynamic table capacity
    insert_count,          %% Next absolute index for insertion
    known_received_count,  %% Entries acknowledged by peer
    encoder_instructions,  %% Pending instructions to send
    last_ric,              %% Required Insert Count from last encode
    pending_sections       %% #{StreamId => queue(RIC)} - for section ack
}).

Static Table

The static table contains 99 pre-defined header fields (indices 0-98) from RFC 9204 Appendix A. It's implemented using three data structures for O(1) lookups:

  • Tuple - Index to entry lookup: element(Index + 1, ?STATIC_TABLE)
  • Field Map - Exact {Name, Value} to index lookup
  • Name Map - Name-only to index lookup

Dynamic Table

The dynamic table uses a combination of:

  • Maps for O(1) lookups by header or name
  • List for FIFO eviction order

Absolute vs Relative Indexing

  • Absolute Index - Monotonically increasing, starts at 0
  • Relative Index - InsertCount - AbsIndex - 1 (most recent = 0)
  • Post-Base Index - AbsIndex - Base (for entries after Base)

Entry Size

Per RFC 9204 Section 3.2.1:

entry_size = name_length + value_length + 32

Encoding Process

  1. Lookup - Check static table, then dynamic table
  2. Decision - Use indexed, literal with name ref, or literal
  3. Insertion - Optionally insert into dynamic table
  4. Instruction Generation - Create encoder stream instructions
  5. Prefix - Encode Required Insert Count and Base

Required Insert Count (RIC)

The RIC indicates the minimum dynamic table state needed to decode:

RIC = max(referenced_absolute_indices) + 1

Encoded as ERIC using modulo arithmetic:

ERIC = (RIC mod (2 * MaxEntries)) + 1

Base Encoding

Base indicates the reference point for relative indices:

  • S=1, DeltaBase=0 means Base = RIC (used by this implementation)
  • S=0 would indicate pre-base references

Decoding Process

  1. Prefix - Decode ERIC and reconstruct RIC using modulo arithmetic
  2. Blocked Check - If RIC > InsertCount, decoding is blocked
  3. Header Lines - Decode each field line using appropriate form
  4. Index Resolution - Convert relative/post-base indices to absolute

Encoder Stream Instructions

InstructionPatternDescription
Insert With Name Ref (static)11xxxxxxReference static table name
Insert With Name Ref (dynamic)10xxxxxxReference dynamic table name
Insert With Literal Name01HxxxxxLiteral name and value
Duplicate000xxxxxDuplicate existing entry
Set Dynamic Table Capacity001xxxxxChange table size

Decoder Stream Instructions

InstructionPatternDescription
Section Acknowledgment1xxxxxxxAcknowledge decoded section
Stream Cancellation01xxxxxxCancel stream
Insert Count Increment00xxxxxxIncrement known received count

Header Field Line Encodings

EncodingPatternDescription
Indexed (static)11xxxxxxStatic table reference
Indexed (dynamic)10xxxxxxDynamic table reference (pre-base)
Indexed (post-base)0001xxxxDynamic table reference (post-base)
Literal with name ref (static)0101xxxxStatic name, literal value
Literal with name ref (dynamic)0100xxxxDynamic name, literal value
Literal with post-base name ref0000NxxxPost-base name reference
Literal with literal name001xHxxxLiteral name and value

Huffman Coding

The Huffman implementation uses a state machine decoder processing 4 bits at a time:

dec_huffman_lookup(State, Nibble) ->
    {ok | more, Char | undefined, NextState}
  • State - Current decoder state (0-255)
  • Nibble - 4-bit input value (0-15)
  • Result - Whether symbol is complete, decoded character, next state

Validation

decode_safe/1 validates per RFC 7541 Section 5.2:

  • Padding must be EOS prefix (all 1s)
  • Padding must not exceed 7 bits
  • EOS symbol must not appear in decoded output

Performance Considerations

  • Static table lookups are O(1) using maps
  • Dynamic table lookups are O(1) using maps
  • Dynamic table eviction is O(n) but amortized across insertions
  • Huffman uses a pre-computed lookup table for speed
  • Binary pattern matching leverages Erlang/OTP optimizations