Skip to content

Fix header table size assignment - #209

Closed
pointlessone wants to merge 1 commit into
igrigorik:mainfrom
pointlessone:fix-header-table-size-assignment
Closed

pointlessone wants to merge 1 commit into
igrigorik:mainfrom
pointlessone:fix-header-table-size-assignment

Conversation

@pointlessone

Copy link
Copy Markdown
Contributor

Wrong connection side's header table size was set.

@HoneyryderChuck

HoneyryderChuck commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Thx for the PR @pointlessone

This proposes a change which reverts this very old commit. I did it back then because of an integration issue with a ruby server, where the browser page wouldn't render. I think I'll need an example in the wild of a peer server which actually fails to process a request. Either that, or references to other HTTP/2 parsers and how they do it.

@pointlessone

Copy link
Copy Markdown
Contributor Author

Now that you mention that fix this turns out to be incomplete.

The order in the PR (and before the linked commit) is the correct order. I was proposing this PR as a fix because in my case I user http-2 as the client (and the commit tried to fix issue from the server side). That commit only masked the issue. The real bug is that table size change is not signalled to the counterpart.

Do not merge this yet. I'll have to look into that a bit closer.

SETTINGS_HEADER_TABLE_SIZE sets the maximum size of the table used by
the encoder on the other side. Our acknowledged setting limits the
peer's encoder, which is our decompressor. The peer's setting limits
our encoder, which is our compressor. 3b51a2e swapped the two.

That commit fixed igrigorik#97, where Chrome sent
SETTINGS_HEADER_TABLE_SIZE=65536 and then failed to decode our headers
with COMPRESSION_ERROR. The swap removed that error, but the assignment
had been correct before it. The actual cause was that the compressor
grew its table to 65536 without sending a dynamic table size update.
Chrome's decoder kept its table at 4096, so our encoder referred to
entries the decoder had already evicted. With the swap, the compressor
stayed at 4096 and the error went away. But a peer that advertised a
smaller table (for example 0) could no longer decode our headers, and
our decoder evicted entries the peer still referred to.

Restore the correct assignment. Change table sizes the way RFC 7541
section 4.2 requires:

- SETTINGS_HEADER_TABLE_SIZE only sets the maximum table size. The
  table changes with a dynamic table size update at the beginning of
  a header block.
- The compressor uses its own table size, capped at the peer's
  maximum. When the size has to change, the next header block starts
  with a size update. If the maximum was reduced and then increased
  between header blocks, it sends the smallest maximum first and then
  the final size.
- The decompressor rejects size updates above the maximum from our
  acknowledged setting. Before, it compared against the initial
  settings. When the maximum is reduced below the current table size,
  the next header block has to start with a size update to the
  smallest maximum or lower.
@pointlessone
pointlessone force-pushed the fix-header-table-size-assignment branch from 3abab39 to fa18ed9 Compare September 30, 2026 16:49
@pointlessone

Copy link
Copy Markdown
Contributor Author

I updated the PR.

As I said before the order of compressor/decompressor is correct in this PR (and was before the linked commit).

The issue is that table size updates are not correctly signalled to the peer. This is a bit tricky part of the protocol. The peer needs to know about the biggest reduction and the final table size in order to correctly evict table entries. So we need to track two numbers: the smallest table size, and the final table size before the next header block. Table size also can be updated only between header blocks so tracking is we started encoding headers or not adds another layer of complexity.


The previous fix worked by accident. Since we didn't acknowledge table size changes to the peer we appear as if we're at the default. Here it worked because we set decoder table size to what came from the peer. It works if peer want to increase the table, which was the case in #97: Chrome tries to set table size to 64k, we increase decompressor table size but do not acknowledge it so Chrome still uses the default 4k. From our point of view Chrome would just happen to not use all table when encoding.

The bug I hit that prompted this PR is from the other side. I use http-2 as a client and I want to increase table size to 64k. Current main would set compressor table size to 64k but not signal it to the peer, they would use 4k table by default and wouldn't be able to decode a bigger table because they would evict entries needed for decoding. In a way, it's a reverse of #97: I'm Chrome trying to send large table and peer can not decode it because they use smaller table.

@HoneyryderChuck HoneyryderChuck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pointlessone I think this is going in the right direction. I'm still rereading that section of the RFC, but the overall flow seems correct. Left some suggestions.

# Set the maximum dynamic table +size+ allowed by the peer's
# SETTINGS_HEADER_TABLE_SIZE. The next header block starts with a
# dynamic table size update when the table size has to change.
def table_size=(size)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this method should be rename to max_table_size, wdyt?

# Set the maximum dynamic table +size+ allowed by our acknowledged
# SETTINGS_HEADER_TABLE_SIZE. When the current table is larger, the
# next header block has to start with a dynamic table size update.
def table_size=(size)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment on lines 113 to +117
@limit = settings.table_size
# Maximum dynamic table size allowed by SETTINGS_HEADER_TABLE_SIZE.
@max_limit = @limit
# Smallest maximum size set since the last dynamic table size update.
@lowest_max_limit = @limit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@limit = settings.table_size
# Maximum dynamic table size allowed by SETTINGS_HEADER_TABLE_SIZE.
@max_limit = @limit
# Smallest maximum size set since the last dynamic table size update.
@lowest_max_limit = @limit
@limit =
# Maximum dynamic table size allowed by SETTINGS_HEADER_TABLE_SIZE.
@max_limit =
# Smallest maximum size set since the last dynamic table size update.
@lowest_max_limit = settings.table_size

(reads slightly better)


# The maximum size was reduced below the current table size and the
# encoder has not signalled a size that fits into it.
raise CompressionError, "dynamic table size update required" if type != :changetablesize && @lowest_max_limit < @limit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this clause can be moved below L163


if @lowest_max_limit < @limit
self.table_size = @lowest_max_limit
yield({ type: :changetablesize, value: @limit })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield({ type: :changetablesize, value: @limit })
yield({ type: :changetablesize, value: @lowest_max_limit })

I know that ultimately it's the same, but it is easier to read.

end
@lowest_max_limit = @max_limit

size = @settings.table_size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 renaming var to max_size or max_table_size would help reading this.

Comment on lines +241 to +244
@lowest_max_limit = @max_limit

size = @settings.table_size
size = @max_limit if @max_limit < size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@lowest_max_limit = @max_limit
size = @settings.table_size
size = @max_limit if @max_limit < size
max_table_size = @lowest_max_limit = @max_limit
max_table_size = @settings.table_size if @settings.table_size > max_table_size

# - https://www.rfc-editor.org/rfc/rfc7541#section-4.2
def max_table_size=(size)
@max_limit = size
@lowest_max_limit = size if size < @lowest_max_limit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a boolean variable set to true here, i.e. @max_table_size_updated = true. This variable should then be used to skip the branch you added into #encode (which should perhaps be moved to a private method), and the same goes for the initial clause you added under #process (... if type != :changetablesize && @max_table_size_updated).

@HoneyryderChuck

Copy link
Copy Markdown
Collaborator

thx again, cherry picked in 0d16d2a

@pointlessone

Copy link
Copy Markdown
Contributor Author

Thank you for merging and sorry for the delay.

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.

2 participants