From 78c0f2c26c100d4eb386d49e517c156914b0ccea Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 11:09:03 +0100 Subject: [PATCH 1/4] run tests against ruby master, run rbs enabled tests in ruby 4 only --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4fcf9f2..6aef42cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.7 ,'3.0', 3.1, 3.2, 3.3, 3.4, 4.0, jruby, truffleruby] + ruby: [2.7 ,'3.0', 3.1, 3.2, 3.3, 3.4, 4.0, head, jruby, truffleruby] steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: RUBY_VERSION=`ruby -e 'puts RUBY_VERSION'` RUBY_PLATFORM=`ruby -e 'puts RUBY_PLATFORM'` RUBY_ENGINE=`ruby -e 'puts RUBY_ENGINE'` - if [[ "$RUBY_ENGINE" = "ruby" ]] && [[ ${RUBY_VERSION:0:1} = "3" ]] && [[ ! $RUBYOPT =~ "jit" ]]; then + if [[ "$RUBY_ENGINE" = "ruby" ]] && [[ ${RUBY_VERSION:0:1} = "4" ]] && [[ ! $RUBYOPT =~ "jit" ]]; then echo "running runtime type checking..." bundle exec rbs collection install export RUBYOPT="-rbundler/setup -rrbs/test/setup" From 88b3ffe45ca101df6ea0f949e879a4b2fcd26deb Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 11:09:38 +0100 Subject: [PATCH 2/4] reinstating hpack conformance tests these have been ignored for a while, as they relied on a git submodule which has been removed a long time ago. reinstating it with a git clone in CI instead. Fixes #200 --- .gitignore | 1 + Rakefile | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 124e4880..90227d58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ Gemfile.lock *.gem +spec/hpack-test-case \ No newline at end of file diff --git a/Rakefile b/Rakefile index a53e41b5..1d32bda0 100644 --- a/Rakefile +++ b/Rakefile @@ -9,16 +9,17 @@ require_relative "tasks/generate_huffman_table" RUBY_MAJOR_MINOR = RUBY_VERSION.split(".").first(2).join(".") -begin - require "rspec/core/rake_task" - RSpec::Core::RakeTask.new(:spec) do |t| - t.exclude_pattern = "./spec/hpack_test_spec.rb" - end +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) do |t| + t.exclude_pattern = "./spec/hpack_test_spec.rb" +end - RSpec::Core::RakeTask.new(:hpack) do |t| - t.pattern = "./spec/hpack_test_spec.rb" - end -rescue LoadError +RSpec::Core::RakeTask.new(:hpack) do |t| + t.pattern = "./spec/hpack_test_spec.rb" +end + +task :prepare_hpack do + system("git clone --depth 1 https://github.com/http2jp/hpack-test-case.git spec/hpack-test-case") end begin @@ -104,6 +105,6 @@ end default_tasks = %i[spec] default_tasks << :rubocop if defined?(RuboCop) && RUBY_ENGINE == "ruby" -default_tasks += %i[h2spec_install h2spec] if ENV.key?("CI") +default_tasks += %i[prepare_hpack hpack h2spec_install h2spec] if ENV.key?("CI") task default: default_tasks -task all: %i[default hpack] +task all: default_tasks From df61497d6c44022a07ab4c24a3a1a9c9bad2745a Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 11:11:51 +0100 Subject: [PATCH 3/4] fix hpack tests to conform to the new state HTTP2::Buffer has been removed a long time ago; moreover, the header field validation layer needs to be bypassed for the conformance tests, since they're using fields that are not approved in practice ("connection" for example). matching arrays shouldn't also rely on order (which http-2 changes to put pseudo-headers first) --- lib/http/2/header/decompressor.rb | 11 +++++++++-- sig/header/decompressor.rbs | 5 +++++ spec/hpack_test_spec.rb | 18 +++++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/http/2/header/decompressor.rb b/lib/http/2/header/decompressor.rb index 9c492412..ddd53067 100644 --- a/lib/http/2/header/decompressor.rb +++ b/lib/http/2/header/decompressor.rb @@ -16,6 +16,11 @@ class Decompressor def initialize(settings = Settings.new) @cc = EncodingContext.new(settings) @max_header_list_size = settings.settings_max_header_list_size + @accept_invalid_headers = false + end + + def accept_invalid_headers! + @accept_invalid_headers = true end # Set the maximum dynamic table +size+ allowed by our acknowledged @@ -110,12 +115,14 @@ def decode(buf, frame = nil) next if field.nil? is_pseudo_header = field.start_with?(":") - if !decoding_pseudo_headers && is_pseudo_header + if !decoding_pseudo_headers && is_pseudo_header && !@accept_invalid_headers raise ProtocolError, "one or more pseudo headers encountered after regular headers" end decoding_pseudo_headers = is_pseudo_header - raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field) + if !@accept_invalid_headers && FORBIDDEN_HEADERS.include?(field) + raise ProtocolError, "invalid header received: #{field}" + end header_list_size += field.bytesize + value.bytesize + 32 raise ProtocolError, "header list exceeds configured maximum" if header_list_size > @max_header_list_size diff --git a/sig/header/decompressor.rbs b/sig/header/decompressor.rbs index a2e614e9..6bdf8d69 100644 --- a/sig/header/decompressor.rbs +++ b/sig/header/decompressor.rbs @@ -7,6 +7,8 @@ module HTTP2 @cc: EncodingContext + @accept_invalid_headers: bool + attr_writer max_header_list_size: Integer def max_table_size=: (Integer) -> void @@ -19,6 +21,9 @@ module HTTP2 def decode: (String, frame?) -> Array[header_pair] | (String) -> Array[header_pair] + + def accept_invalid_headers!: () -> void + private def initialize: (?Settings settings) -> void diff --git a/spec/hpack_test_spec.rb b/spec/hpack_test_spec.rb index 083ced14..f1a6dbe4 100644 --- a/spec/hpack_test_spec.rb +++ b/spec/hpack_test_spec.rb @@ -36,12 +36,14 @@ story = JSON.parse(File.read("#{path}/#{file}")) cases = story["cases"] table_size = cases[0]["header_table_size"] || 4096 - @dc = Decompressor.new(table_size: table_size) + settings = Settings.new(table_size: table_size) + @dc = Decompressor.new(settings) + @dc.accept_invalid_headers! cases.each do |c| wire = [c["wire"]].pack("H*").force_encoding(Encoding::BINARY) - @emitted = @dc.decode(HTTP2::Buffer.new(wire)) + @emitted = @dc.decode(wire) headers = c["headers"].flat_map(&:to_a) - expect(@emitted).to eq headers + expect(@emitted).to match_array(headers) end end end @@ -64,6 +66,7 @@ [4096, 512].each do |table_size| options = { table_size: table_size } options.update(encoding_options) + settings = HTTP2::Settings.new(**options) context "with #{mode}#{huffman} mode and table_size #{table_size}" do path = File.expand_path("hpack-test-case/raw-data", File.dirname(__FILE__)) @@ -73,13 +76,14 @@ it "should encode #{file}" do story = JSON.parse(File.read("#{path}/#{file}")) cases = story["cases"] - @cc = Compressor.new(options) - @dc = Decompressor.new(options) + @cc = Compressor.new(settings) + @dc = Decompressor.new(settings) + @dc.accept_invalid_headers! cases.each do |c| headers = c["headers"].flat_map(&:to_a) wire = @cc.encode(headers) - decoded = @dc.decode(HTTP2::Buffer.new(wire)) - expect(decoded).to eq headers + decoded = @dc.decode(wire) + expect(decoded).to match_array(headers) end end end From 2ac76c4f1651bb1418d9531b14842f13866003e3 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 6 Oct 2026 16:03:24 +0100 Subject: [PATCH 4/4] set the max size limit of encoding context to the max header table size defined by the protocol --- lib/http/2/header/encoding_context.rb | 12 ++++++------ lib/http/2/settings.rb | 7 ++++--- sig/settings.rbs | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/http/2/header/encoding_context.rb b/lib/http/2/header/encoding_context.rb index 04e62a4c..0c97e9ae 100644 --- a/lib/http/2/header/encoding_context.rb +++ b/lib/http/2/header/encoding_context.rb @@ -110,11 +110,11 @@ def initialize(settings = Settings.new) @unshifts = 0 @settings = settings # Current dynamic table size, as known to both encoder and decoder. - @limit = - # Maximum dynamic table size allowed by SETTINGS_HEADER_TABLE_SIZE. - @max_size_limit = - # Smallest maximum size set since the last dynamic table size update. - @lowest_max_size_limit = settings.table_size + @limit = settings.table_size + # Maximum dynamic table size allowed by SETTINGS_HEADER_TABLE_SIZE. + @max_size_limit = + # Smallest maximum size set since the last dynamic table size update. + @lowest_max_size_limit = MAX_HEADER_TABLE_SIZE @_table_updated = @_max_table_size_updated = false @current_table_size = 0 end @@ -340,7 +340,7 @@ def encode_change_table_size end @lowest_max_size_limit = @max_size_limit - max_table_size = @settings.table_size + max_table_size = MAX_HEADER_TABLE_SIZE max_table_size = @max_size_limit if @max_size_limit < max_table_size return unless max_table_size != @limit diff --git a/lib/http/2/settings.rb b/lib/http/2/settings.rb index 13dca924..0332b232 100644 --- a/lib/http/2/settings.rb +++ b/lib/http/2/settings.rb @@ -2,10 +2,11 @@ module HTTP2 MAX_HEADER_LIST_SIZE = (2 << 30) - 1 + MAX_HEADER_TABLE_SIZE = 4096 # Default values for SETTINGS frame, as defined by the spec. SPEC_DEFAULT_CONNECTION_SETTINGS = { - settings_header_table_size: 4096, + settings_header_table_size: MAX_HEADER_TABLE_SIZE, settings_enable_push: 1, # enabled for servers settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited settings_initial_window_size: 65_535, @@ -31,7 +32,7 @@ module HTTP2 ) do def initialize( # connection settings - settings_header_table_size: 4096, + settings_header_table_size: MAX_HEADER_TABLE_SIZE, settings_enable_push: 1, settings_max_concurrent_streams: 100, settings_initial_window_size: 65_535, @@ -40,7 +41,7 @@ def initialize( # encoding context settings huffman: :shorter, index: :all, - table_size: 4096 + table_size: MAX_HEADER_TABLE_SIZE ) super end diff --git a/sig/settings.rbs b/sig/settings.rbs index 40d51490..8ed922fe 100644 --- a/sig/settings.rbs +++ b/sig/settings.rbs @@ -1,6 +1,8 @@ module HTTP2 MAX_HEADER_LIST_SIZE: Integer + MAX_HEADER_TABLE_SIZE: Integer + SPEC_DEFAULT_CONNECTION_SETTINGS: Hash[Symbol, Integer] ENCODING_CONTEXT_SETTINGS: Array[Symbol]