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.