QPACK is a header field compression format for HTTP/3 that makes HTTP/2’s HPACK header compression format compatible with the QUIC protocol.

In HTTP/3, the way the sender’s encoder and the receiver’s decoder reach agreement on the the state of the dynamic table for compression would be very different compared to HTTP/2. In this post, I would like to talk some of understanding I gained while I was reading the RFC and implementing QPACK.

Why Different?

HPACK doesn’t need the endpoint to communicate directly with its peer for the most of time (unless there is a change in the dynamic table size). Since all HTTP/2 frames in different stream will always be transmitted in order when they are transmitted over TCP. So we don’t need to worry if the decoder’s dynamic table will somehow get into a state different from the encoder’s at all.

Nevertheless, due to the nature of QUIC, which only guarantees that data in the same stream would “arrive in order” at the application layer, QPACK requires the sender’s encoder to start a HTTP/3 unidirectional stream Encoder Stream and the receiver’s decoder to start a HTTP/3 unidirectional stream Decoder Stream to sync the states of the dynamic table at both sides.

Certain encoder instructions and decoder instructions are consequently defined in QPACK’s RFC to modify the peer’s state.

Dynamic Table

How dynamic tables are maintained on its own has no big differences compared to HPACK.

It’s still a FIFO list with a max capacity. Any entries referenced in the HEADERS frames will be marked and can not be evicted whereas the unreferenced entries thus can be evicted if space is needed when inserting new entries.

The initial capacity of the dynamic table is going to be zero at both sides. If the receiver’s decoder broadcasts its setting SETTINGS_QPACK_MAX_TABLE_CAPACITY with a non-zero value and the sender’s encoder has its maximum capacity max_cap a non-zero value as well, then the encoder can determine the dynamic table capacity to be used in this pair no more than min(SETTINGS_QPACK_MAX_TABLE_CAPACITY, max_cap).

Instructions

Instructions are defined to help sync the states of the dynamic table at both sides. Note that if dynamic table is not going to be in the connection, then these instruction are not necessarily being used. All these instructions will be sent unframed through QUIC’s unidirectional stream.

Encoder Instructions

Encoder can emit four kinds of instructions of two categories:

  • Instruction Set Dynamic Table Capacity. This will make the decoder to change its dynamic table capacity. A typical use case would be, the sender sets its actual dynamic capacity after receiving the SETTINGS frames. It will rely on this instruction to let the decoder change its initial capacity to the same as the encoder’s (RFC9204 section.3.2.2).

  • Instruction Insert with Name Reference, Insert with Literal Name and Duplicate. After the sender inserts a new entry to its dynamic table. One of these instructions would be emitted, the decoder will insert the same entry to its dynamic table by decoding the instruction. The exact type of instruction would be used depends on the state before the new entry was inserted into the encoder’s dynamic table. If name:value can be found, then a Duplicate would be used. If only name can be found, Insert with Name Reference will be used. Otherwise, the instruction will be Insert with Literal Name.

Decoder Instructions

The instructions sent by the decoder would be mainly for the decoder to notify the encoder on different events in HTTP/3 and QPACK:

  • Instruction Section Acknowledgment. It’s would be emitted once the decoder successfully decodes the field sections in HEADERS frames in the stream which are referred to the encoder’s dynamic table entries. Then the encoder can clear the references to the corresponding entries in its dynamic table.

  • Instruction Insert Count Increment. This instruction is quite similar to the previous one, used for acknowledgement that new entries have been received by the decoder and properly inserted into its dynamic table. Once the encoder received this instruction, then it can know that what entries it can safely use for encoding without potentially blocking the request/push stream.

  • Instruction Stream Cancellation. Once the receiver decides to reset the stream for the reasons other before properly close the stream (i.e., a client resets a stream because the frames sequence on that stream is wrong), the endpoint needs to emit this instruction to notify the sender’s encoder that all references associated with this stream should be removed.

A Typical Session

To give a better and clearer idea how QPACK works between two endpoints, below is a sample sequence diagram. Since there are going to be two pairs of QPACK’s encoder and decoder between client and server’s HTTP/3 connection, here we focus on the client’s encoder and the server’s decoder, as the other pair will behave the same.

sequenceDiagram
    box client
    participant Client's encoder
    participant Client
    end

    box server
    participant Server
    participant Server's decoder
    end

    Client->>Server: Control Stream, Client's SETTINGS frame

    rect rgb(93, 173, 226)
    Server->>Client: Control Stream, Server's SETTINGS frame 
(contains Non-zero SETTINGS_QPACK_MAX_TABLE_CAPACITY) Client's encoder->>Server's decoder: Starts Encoder Stream, and
sends Set Dynamic Table Capacity instruction to enable dynamic table Server's decoder->>Client's encoder: Starts Decoder stream. (Server can also wait until
there are entries inserted into the dynamic table then start this) end Server-->Client: The same actions happens between
the client's decoder and server's encoder rect rgb(93, 173, 226) Client-->>Server: Starts a request stream with an id of 4 Client->>Client's encoder: encode the fields for
the header fields in the request on stream 4 Client's encoder->>Client: Encodes the fields by inserting the fields into
dynamic table and add references
to those entries for stream 4 Client's encoder->>Server's decoder: Sends out Insert Instructions to update decoder's dynamic table par Client-->>Server: Sends out the HTTP request on stream 4
with encoded HEADERS frame Note right of Client: Client may use the entries
before receiving the ack Server's decoder->>Client's encoder: Sends out Insert Count Increment instruction to acknowledge its dynamic table update end Server->>Server's decoder: Decodes the HEADERS
frames on stream 4 Server's decoder->>Client's encoder: Sends out Section Acknowledgment instruction
after it decodes all fields in the received HEADER frame on stream 4 Client's encoder->>Client's encoder: Clears the references
associated with stream 4 end Client-->Server: Following client's requests on request streams
of the same connection will repeat the processes above

References