cadlag dot org

HTTP/2: Header Compression With HPACK

HTTP/1.1 requests and response typically carry a header which carries metadata about the HTTP message. The header contains key-value pairs that appear right after the start line of the message, but before the actual data. For example, the following message (from the MDN docs) has a header with three entries:

POST /users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 50

name=FirstName%20LastName&email=bsmth%40example.com

In the above example, the header is larger than the actual body of the request. Worse still, HTTP is stateless, so session metadata (e.g. cookies) must be replicated for every request.

HTTP/2 improves upon this situation by introducing a header compression format known as HPACK. Introduced in RFC 7541, HPACK manages key-value pairs through a shared table which can be updated during the lifespan of a persistent HTTP/2 connection. A client can mark that a key-value pair should be added to the table, and subsequently refer to either the key or the key & value by their index into this table.

HPACK provides a mechanism but leaves the compression strategy up to the implementation (several flavors of dictionary coding are possible). The sender has full control over when and how the table is managed; a receiver simply decodes messages according to the usage of the sender.

To illustrate HPACK in action, we can step through an example given in the appendix of RFC 7541. We’ll skip some of the bitwise minutiae, instead just following the high-level logic.

Suppose a client makes an initial request with the following headers:

:method: GET
:scheme: http
:path: /
:authority: www.example.com

The first three key-value pairs are so common that rather than require the client to introduce them explicitly, they correspond to entries 2, 6, and 4 in a pre-defined static table. On the wire they only require three bytes: 0x82 (0x80 + offset 2) for the first, 0x86 for the second, and 0x84 for the third.

Things get interesting with the next pair, :authority: www.example.com. Here we have a very common header key that appears in the static table (:authority: shows up with index 1), but the value (www.example.com) is new to this connection. In such cases, HPACK lets you refer to the key as a standalone entity and then provide the value directly. On the wire this looks like 0x41 (“just the key with index 1”), followed by 0x0f (indicating the value is a 15 byte literal), followed by the byte values for www.example.com.

Taken together, the header for this first message takes 20 bytes to represent with HPACK. Considering that the first :method: GET, followed by a carriage return and line feed, is 14 bytes under HTTP/1.1, I’d say HPACK is pretty effective in this example.

The real power of HPACK comes from the fact that it provides for a dynamic key-value table which persists across the lifetime of the HTTP connection. Initially this table is empty, and after this first request the dynamic table would have a single entry:

Index Key Value
62 :authority: www.example.com

Note that the static table uses indices 1-61, so the dynamic table starts at 62.

Suppose now the client makes a second request with the following header:

:method: GET
:scheme: http
:path: /
:authority: www.example.com
cache-control: no-cache

As before, the first three entries are compressed as 0x82 86 84 due to their presence in the static table. But now the :authority www.example.com pair is present with index 62, so the client can refer to it with 0xbe (this is 0x80 + 62).

The last line, cache-control: no-cache, has a key that appears in the static table (index 24) but a value that does not. So it is represented as 0x58 (for the key) followed by 0x08 (designating an eight character string literal) and then the actual encoding of the string no-cache.

As we see, this entire header is represented in just 14 bytes under HPACK. After it is sent the dynamic table has a new entry for cache-control: no-cache. By convention, new entries are inserted at the head of this table (shifting the other entries by 1):

Index Key Value
62 cache-control: no-cache
63 :authority: www.example.com

The specification requires that the entire static+dynamic table have a fixed size, to make decoding inexpensive. This is controllable via an HTTP/2 parameter SETTINGS_HEADER_TABLE_SIZE which defaults to 256. Concretely, this means that inserting new entries to the table can cause old ones to roll off.

As you can see, HPACK provides a simple but effective mechanism for header compression. A session cookie would only need to be sent once, after which it would occupy a single entry in the dynamic table and could be referred to by index. This keeps headers small, so that network resources may be more efficiently be used by application data.

Reply to this post by email ↪