From cb54367ade4976aa5cc8024687a0af4d934f12d6 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 12:13:22 +0100 Subject: [PATCH 1/6] settings stored in a struct instead of a hash this changes the internal representation of settings to a struct, instead of a hash. The advantages are clearer APIs and type definitions, better performance (set/get are faster), as well as better storage. --- lib/http/2.rb | 1 + lib/http/2/client.rb | 2 +- lib/http/2/connection.rb | 39 ++++++++----------------------- lib/http/2/header/decompressor.rb | 2 +- lib/http/2/settings.rb | 36 ++++++++++++++++++++++++++++ sig/2.rbs | 13 ----------- sig/connection.rbs | 4 ++-- sig/settings.rbs | 26 +++++++++++++++++++++ 8 files changed, 77 insertions(+), 46 deletions(-) create mode 100644 lib/http/2/settings.rb create mode 100644 sig/settings.rbs diff --git a/lib/http/2.rb b/lib/http/2.rb index db840643..a0f94e23 100644 --- a/lib/http/2.rb +++ b/lib/http/2.rb @@ -13,6 +13,7 @@ module HTTP2 require "http/2/flow_buffer" require "http/2/header" require "http/2/framer" +require "http/2/settings" require "http/2/connection" require "http/2/client" require "http/2/server" diff --git a/lib/http/2/client.rb b/lib/http/2/client.rb index 6529359b..04e51941 100644 --- a/lib/http/2/client.rb +++ b/lib/http/2/client.rb @@ -64,7 +64,7 @@ def send_connection_preface @state = :connected emit(:frame, CONNECTION_PREFACE_MAGIC) - payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] } + payload = @local_settings.each_pair.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] } settings(payload) end diff --git a/lib/http/2/connection.rb b/lib/http/2/connection.rb index 70d16495..79a96472 100644 --- a/lib/http/2/connection.rb +++ b/lib/http/2/connection.rb @@ -10,25 +10,6 @@ module HTTP2 # Default stream_limit DEFAULT_MAX_CONCURRENT_STREAMS = 100 - # Default values for SETTINGS frame, as defined by the spec. - SPEC_DEFAULT_CONNECTION_SETTINGS = { - settings_header_table_size: 4096, - settings_enable_push: 1, # enabled for servers - settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited - settings_initial_window_size: 65_535, - settings_max_frame_size: 16_384, - settings_max_header_list_size: (2 << 30) - 1 # unlimited - }.freeze - - DEFAULT_CONNECTION_SETTINGS = { - settings_header_table_size: 4096, - settings_enable_push: 1, # enabled for servers - settings_max_concurrent_streams: 100, - settings_initial_window_size: 65_535, - settings_max_frame_size: 16_384, - settings_max_header_list_size: (2 << 30) - 1 # unlimited - }.freeze - # Default stream priority (lower values are higher priority). DEFAULT_WEIGHT = 16 @@ -80,8 +61,8 @@ class Connection # Initializes new connection object. # def initialize(settings = {}) - @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings) - @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup + @local_settings = Settings.new(**settings) + @remote_settings = Settings.new(settings_max_concurrent_streams: Framer::MAX_STREAM_ID) @compressor = Header::Compressor.new(settings) @decompressor = Header::Decompressor.new(settings) @@ -93,11 +74,11 @@ def initialize(settings = {}) @oldest_stream_recently_closed = nil @pending_settings = [] - @framer = Framer.new(@local_settings[:settings_max_frame_size]) + @framer = Framer.new(@local_settings.settings_max_frame_size) - @local_window_limit = @local_settings[:settings_initial_window_size] + @local_window_limit = @local_settings.settings_initial_window_size @local_window = @local_window_limit - @remote_window_limit = @remote_settings[:settings_initial_window_size] + @remote_window_limit = @remote_settings.settings_initial_window_size @remote_window = @remote_window_limit @recv_buffer = "".b @@ -137,7 +118,7 @@ def new_stream(**args) stream = activate_stream( id: @stream_id, - max_concurrent_streams: @remote_settings[:settings_max_concurrent_streams], + max_concurrent_streams: @remote_settings.settings_max_concurrent_streams, **args ) @last_stream_id = stream.id @@ -213,7 +194,7 @@ def receive(data) elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC # MAGIC is OK. Send our settings @state = :waiting_connection_preface - payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] } + payload = @local_settings.each_pair.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] } settings(payload) else raise HandshakeError @@ -254,7 +235,7 @@ def receive(data) # prevent HTTP/2 CONTINUATION FLOOD # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood # different mitigation (connection closed, instead of 400 response) - unless @continuation_size < @local_settings[:settings_max_frame_size] + unless @continuation_size < @local_settings.settings_max_frame_size connection_error(:protocol_error, msg: "too many continuations received") end @@ -752,7 +733,7 @@ def encode_headers(headers_frame) #: @type var payload: String headers_frame[:payload] = payload - max_frame_size = @remote_settings[:settings_max_frame_size] + max_frame_size = @remote_settings.settings_max_frame_size # if single frame, return immediately if payload.bytesize <= max_frame_size @@ -796,7 +777,7 @@ def encode_headers(headers_frame) # @param priority [Integer] # @param window [Integer] # @param parent [Stream] - def activate_stream(id:, max_concurrent_streams: @local_settings[:settings_max_concurrent_streams], **args) + def activate_stream(id:, max_concurrent_streams: @local_settings.settings_max_concurrent_streams, **args) connection_error(msg: "Stream ID already exists") if @streams.key?(id) # SETTINGS_MAX_CONCURRENT_STREAMS limits the number of concurrent streams that the sender diff --git a/lib/http/2/header/decompressor.rb b/lib/http/2/header/decompressor.rb index 6f0385bb..4a642860 100644 --- a/lib/http/2/header/decompressor.rb +++ b/lib/http/2/header/decompressor.rb @@ -115,7 +115,7 @@ def header(buf) # @param frame [HTTP2::Frame, nil] # @return [Array] +[[name, value], ...] def decode(buf, frame = nil) - list = [] + list = [] #: Array[header_pair] decoding_pseudo_headers = true @cc.listen_on_table do until buf.empty? diff --git a/lib/http/2/settings.rb b/lib/http/2/settings.rb new file mode 100644 index 00000000..3f7449f9 --- /dev/null +++ b/lib/http/2/settings.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module HTTP2 + MAX_HEADER_LIST_SIZE = (2 << 30) - 1 + + # Default values for SETTINGS frame, as defined by the spec. + SPEC_DEFAULT_CONNECTION_SETTINGS = { + settings_header_table_size: 4096, + settings_enable_push: 1, # enabled for servers + settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited + settings_initial_window_size: 65_535, + settings_max_frame_size: 16_384, + settings_max_header_list_size: MAX_HEADER_LIST_SIZE # unlimited + }.freeze + + Settings = Struct.new( + :settings_header_table_size, + :settings_enable_push, + :settings_max_concurrent_streams, + :settings_initial_window_size, + :settings_max_frame_size, + :settings_max_header_list_size, + keyword_init: true + ) do + def initialize( + settings_header_table_size: 4096, + settings_enable_push: 1, + settings_max_concurrent_streams: 100, + settings_initial_window_size: 65_535, + settings_max_frame_size: 16_384, + settings_max_header_list_size: MAX_HEADER_LIST_SIZE + ) + super + end + end +end diff --git a/sig/2.rbs b/sig/2.rbs index 81fac9b0..b747d8b2 100644 --- a/sig/2.rbs +++ b/sig/2.rbs @@ -11,23 +11,10 @@ module HTTP2 type connection_opts = Hash[Symbol, untyped] - type settings_hash = { - settings_header_table_size: Integer, - settings_enable_push: Integer, - settings_max_concurrent_streams: Integer, - settings_initial_window_size: Integer, - settings_max_frame_size: Integer, - settings_max_header_list_size: Integer - } - type settings_ary = Array[settings_enum] type settings_enum = Enumerable[[Symbol, Integer]] - SPEC_DEFAULT_CONNECTION_SETTINGS: settings_hash - - DEFAULT_CONNECTION_SETTINGS: settings_hash - DEFAULT_WEIGHT: Integer CONNECTION_PREFACE_MAGIC: String diff --git a/sig/connection.rbs b/sig/connection.rbs index 38ca5fb8..21c344e1 100644 --- a/sig/connection.rbs +++ b/sig/connection.rbs @@ -21,8 +21,8 @@ module HTTP2 alias window local_window - attr_reader remote_settings: settings_hash - attr_reader local_settings: settings_hash + attr_reader remote_settings: Settings + attr_reader local_settings: Settings attr_reader pending_settings: settings_ary attr_accessor active_stream_count: Integer diff --git a/sig/settings.rbs b/sig/settings.rbs new file mode 100644 index 00000000..27bee8a6 --- /dev/null +++ b/sig/settings.rbs @@ -0,0 +1,26 @@ +module HTTP2 + MAX_HEADER_LIST_SIZE: Integer + + SPEC_DEFAULT_CONNECTION_SETTINGS: Hash[Symbol, Integer] + + class Settings # this is actually a Struct, but RBS does not support it yet + attr_reader settings_header_table_size: Integer + attr_reader settings_enable_push: Integer + attr_reader settings_max_concurrent_streams: Integer + attr_reader settings_initial_window_size: Integer + attr_reader settings_max_frame_size: Integer + attr_reader settings_max_header_list_size: Integer + + def initalize: ( + ?settings_header_table_size: Integer, + ?settings_enable_push: Integer, + ?settings_max_concurrent_streams: Integer, + ?settings_initial_window_size: Integer, + ?settings_max_frame_size: Integer, + ?settings_max_header_list_size: Integer + ) -> void + + def each_pair: () { (Symbol, Integer) -> void } -> self + | () -> Enumerable[[Symbol, Integer]] + end +end \ No newline at end of file From 24dff64993a9cdc5f745e0400f84dbee9f99e39f Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 12:15:22 +0100 Subject: [PATCH 2/6] rbs collection install in CI --- .github/workflows/ci.yml | 1 + rbs_collection.yaml | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 rbs_collection.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e7194e2..d4fcf9f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: RUBY_ENGINE=`ruby -e 'puts RUBY_ENGINE'` if [[ "$RUBY_ENGINE" = "ruby" ]] && [[ ${RUBY_VERSION:0:1} = "3" ]] && [[ ! $RUBYOPT =~ "jit" ]]; then echo "running runtime type checking..." + bundle exec rbs collection install export RUBYOPT="-rbundler/setup -rrbs/test/setup" export RBS_TEST_RAISE="true" export RBS_TEST_LOGLEVEL="error" diff --git a/rbs_collection.yaml b/rbs_collection.yaml new file mode 100644 index 00000000..0eaf9d9b --- /dev/null +++ b/rbs_collection.yaml @@ -0,0 +1,21 @@ +# Download sources +sources: + - type: git + name: ruby/gem_rbs_collection + remote: https://github.com/ruby/gem_rbs_collection.git + revision: main + repo_dir: gems + +# You can specify local directories as sources also. +# - type: local +# path: path/to/your/local/repository + +# A directory to install the downloaded RBSs +path: .gem_rbs_collection + +gems: + # If you want to avoid installing rbs files for gems, you can specify them here. + - name: rbs + ignore: true + - name: steep + ignore: true From 7fe18975d1cc53fd2cbc84a584e346b7db9a29f0 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 12:18:40 +0100 Subject: [PATCH 3/6] add irb to Gemfile on ruby 4 not standard gem anymore --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 0f51e512..832c3c08 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ group :development do gem "rubocop" gem "rubocop-performance" end + gem "irb" if RUBY_VERSION >= "4.0.0" end group :docs do From 63c3190e3a476eac648b1b16e96af3410ad51a67 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 12:23:21 +0100 Subject: [PATCH 4/6] fix sig for connection_management it may receive anything with stream 0 --- sig/connection.rbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sig/connection.rbs b/sig/connection.rbs index 21c344e1..881e2577 100644 --- a/sig/connection.rbs +++ b/sig/connection.rbs @@ -87,7 +87,7 @@ module HTTP2 def connection_frame?: (frame) -> bool - def connection_management: (connection_frame frame) -> void + def connection_management: (frame frame) -> void def ping_management: (frame) -> void From f2e927a9f17f42a728f6e25528c1ab64261571e8 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 12:40:10 +0100 Subject: [PATCH 5/6] memoizing calc of encoded header size (used twice) --- lib/http/2/header/encoding_context.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/http/2/header/encoding_context.rb b/lib/http/2/header/encoding_context.rb index 0dc06c56..b00c4eb5 100644 --- a/lib/http/2/header/encoding_context.rb +++ b/lib/http/2/header/encoding_context.rb @@ -217,11 +217,12 @@ def process(cmd) emit = [name, value] # add to table - if type == :incremental && size_check?(name.bytesize + value.bytesize + 32) + cmdsize = name.bytesize + value.bytesize + 32 + if type == :incremental && size_check?(cmdsize) @table.unshift(emit) @unshifts += 1 @table_by_field[name].unshift([value, @unshifts]) - @current_table_size += name.bytesize + value.bytesize + 32 + @current_table_size += cmdsize @_table_updated = true end From 88bddbb595962c8a51d26562c1dc1e167134a3a0 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 10 Sep 2026 14:27:25 +0100 Subject: [PATCH 6/6] improving sigs --- lib/http/2/server.rb | 2 +- sig/2.rbs | 6 ++++++ sig/connection.rbs | 10 ---------- sig/server.rbs | 2 ++ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/http/2/server.rb b/lib/http/2/server.rb index c7c5bd3d..c233b493 100644 --- a/lib/http/2/server.rb +++ b/lib/http/2/server.rb @@ -97,7 +97,7 @@ def upgrade(settings, headers, body) dependency: 0, exclusive: false, payload: headers - } + } #: headers_frame if body.empty? headers_frame[:flags] |= END_STREAM diff --git a/sig/2.rbs b/sig/2.rbs index b747d8b2..eefe7a06 100644 --- a/sig/2.rbs +++ b/sig/2.rbs @@ -23,6 +23,12 @@ module HTTP2 RESPONSE_MANDATORY_HEADERS: Array[String] + CONNECTION_FRAME_TYPES: Array[Symbol] + + STREAM_OPEN_STATES: Array[Symbol] + + HEADERS_FRAME_TYPES: Array[Symbol] + # Frame flags END_STREAM: Integer ACK: Integer diff --git a/sig/connection.rbs b/sig/connection.rbs index 881e2577..f00f8d74 100644 --- a/sig/connection.rbs +++ b/sig/connection.rbs @@ -4,16 +4,6 @@ module HTTP2 include Emitter include BufferUtils - REQUEST_MANDATORY_HEADERS: Array[String] - - RESPONSE_MANDATORY_HEADERS: Array[String] - - CONNECTION_FRAME_TYPES: Array[Symbol] - - HEADERS_FRAME_TYPES: Array[Symbol] - - STREAM_OPEN_STATES: Array[Symbol] - attr_reader state: Symbol attr_reader local_window: Integer diff --git a/sig/server.rbs b/sig/server.rbs index dd46e5c0..4317c9b2 100644 --- a/sig/server.rbs +++ b/sig/server.rbs @@ -1,5 +1,7 @@ module HTTP2 class Server < Connection + @origin_set: Array[String] + @origins_sent: bool def upgrade: (String settings, Enumerable[header_pair] headers, String body) -> void