Add HPAX.protocol_resize/2 for decoding tables - #32
Merged
Merged
Conversation
Coverage Report for CI Build 325Coverage increased (+1.9%) to 97.895%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
馃挍 - 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
force-pushed
the
decoder-table-size-rules
branch
from
September 20, 2026 23:03
21dc3f9 to
325b357
Compare
ericmj
marked this pull request as ready for review
September 20, 2026 23:15
whatyouhide
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
resize/2sets bothprotocol_max_table_sizeandmax_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/2changes 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", sodecode/2returns{: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/2parses 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/2keeps 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 toprotocol_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.