Skip to content

Add HPAX.protocol_resize/2 for decoding tables - #32

Merged
ericmj merged 2 commits into
mainfrom
decoder-table-size-rules
Sep 21, 2026
Merged

ericmj merged 2 commits into
mainfrom
decoder-table-size-rules

Conversation

@ericmj

@ericmj ericmj commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

resize/2 sets both protocol_max_table_size and max_table_size. That's right for an encoding context, which takes the whole size the protocol permits and declares it with a dynamic table size update, but a decoding table's maximum size belongs to the peer's encoder: RFC 7541 4.2 says the protocol determines "the maximum size that the encoder is permitted to use", while "a change in the maximum size of the dynamic table is signaled via a dynamic table size update".

So raising the permitted maximum on a decoding table also raised the table's own maximum and kept entries the peer had evicted. With the peer's encoder on a 64 byte table holding :status: 404, a raise to 8192, and the peer then storing :status: 500, which evicts 404 from its own table, an indexed reference to entry 63 decoded to the stale 404 instead of failing. RFC 9113 4.3 makes a decoding error a connection error of type COMPRESSION_ERROR, so the block should have been rejected.

protocol_resize/2 changes only the permitted maximum and follows it down when it drops below the maximum the encoder declared, evicting to fit. The encoder then has to declare a size of at most the new maximum at the start of its next block, and RFC 9113 4.3.1 requires the receiver to "treat a field block that follows an acknowledgment of the reduction to the maximum dynamic table size as a connection error of type COMPRESSION_ERROR if it does not start with a conformant Dynamic Table Size Update instruction", so decode/2 returns {:error, :missing_size_update} for a block that doesn't. When the maximum changes more than once before that block, the smallest of them is the one that has to be declared, per RFC 7541 4.2.

The second commit is separate and pre-existing: decode/2 parses the size update's integer before the clause that catches decoding errors, so a block whose integer never ends, such as <<0x3F>>, threw {:hpax, :bad_integer_encoding} at the caller instead of returning it as an error tuple. That throw escapes callers that follow the documented contract.

resize/2 keeps its behavior for encoding contexts and its docs now say so. The @doc since: "1.1.0" tag assumes 1.1.0 is the next release. CHANGELOG.md is untouched, since this repo writes it in the release commit.

Found while reviewing elixir-mint/mint#507, which makes put_settings(conn, header_table_size: n) usable for the first time and is the path that reaches this. With Mint pointed at this branch and its decode table switched to protocol_resize/2, the three cases come back as {:compression_error, "unable to decode headers: :missing_size_update"} and {:compression_error, "unable to decode headers: {:index_not_found, 63}"}, and Mint's 147 HTTP/2 connection tests still pass.

@coveralls

coveralls commented Sep 20, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 325

Coverage increased (+1.9%) to 97.895%

Details

  • Coverage increased (+1.9%) from the base build.
  • Patch coverage: 17 of 17 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 190
Covered Lines: 186
Line Coverage: 97.89%
Coverage Strength: 3991.66 hits per line

馃挍 - Coveralls

resize/2 sets both the protocol maximum and the table's own maximum
size. That's right for an encoding context, which takes the whole size
the protocol permits and declares it, but a decoding table's maximum
size is chosen by the peer's encoder and declared with a dynamic table
size update instruction (RFC 7541 4.2). Raising the protocol maximum on
a decoding table kept entries the peer's encoder had evicted, so an
indexed reference to one of them decoded to a stale header instead of
failing.

protocol_resize/2 changes only the maximum the peer is permitted to use
and follows it down when it drops below the maximum the encoder
declared. The encoder then has to declare a size of at most the new
maximum at the start of its next block, which RFC 9113 4.3.1 requires
the receiver to enforce with a COMPRESSION_ERROR, so decode/2 returns
{:error, :missing_size_update} for a block that doesn't start with one.
When the maximum changes more than once before that block, the smallest
of them is the one that has to be declared.
decode/2 parses the size update's integer before the clause that catches
decoding errors, so a block whose integer never ends, such as <<0x3F>>,
threw {:hpax, :bad_integer_encoding} at the caller instead of returning
{:error, :bad_integer_encoding}. Callers that follow the documented
contract don't catch it, so the throw takes down the process decoding
the block.
@ericmj
ericmj force-pushed the decoder-table-size-rules branch from 21dc3f9 to 325b357 Compare September 20, 2026 23:03
@ericmj
ericmj marked this pull request as ready for review September 20, 2026 23:15
@ericmj
ericmj merged commit f7d69fc into main Sep 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants