From 4c398e69ab1ea5523ccc7b73abdfd19a8e6bc3f2 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 14 Oct 2025 09:32:52 +0100 Subject: [PATCH 1/3] ignore ping frame when connection is closed while it's valid to process ping frames after sending/receiving goaway frames, preparing an AC frame may inadvertedly put bytes on the user buffer, thereby signaling that there's something to write back instead of terminating the connection --- lib/http/2/connection.rb | 2 -- spec/shared_examples/connection.rb | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/http/2/connection.rb b/lib/http/2/connection.rb index d3cf180..1b97d3e 100644 --- a/lib/http/2/connection.rb +++ b/lib/http/2/connection.rb @@ -527,8 +527,6 @@ def connection_management(frame) when :goaway # 6.8. GOAWAY # An endpoint MAY send multiple GOAWAY frames if circumstances change. - when :ping - ping_management(frame) else connection_error if (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @closed_since) > 15 end diff --git a/spec/shared_examples/connection.rb b/spec/shared_examples/connection.rb index a2e9f20..b43683f 100644 --- a/spec/shared_examples/connection.rb +++ b/spec/shared_examples/connection.rb @@ -135,6 +135,16 @@ expect(pong).to eq "12345678" end + it "should not fire callback on PONG if connection is closed" do + conn << f.generate(settings_frame) + conn << f.generate(goaway_frame) + + pong = nil + conn.ping("12345678") { |d| pong = d } + conn << f.generate(pong_frame) + expect(pong).to be_nil + end + it "should fire callback on receipt of GOAWAY" do last_stream, payload, error = nil conn << f.generate(settings_frame) From 071b5755bc79009cccec94edf8bab649209e6b9a Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 14 Oct 2025 10:37:20 +0100 Subject: [PATCH 2/3] example server: do not delay closing socket on goaway frame this causes the truffleruby build to fail, as it expects to either receive a ping ack or have the connection closed --- example/server.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/example/server.rb b/example/server.rb index cb97d83..555326a 100644 --- a/example/server.rb +++ b/example/server.rb @@ -62,10 +62,7 @@ end conn.on(:goaway) do - Thread.start do - sleep(1) - sock.close - end + sock.close end conn.on(:stream) do |stream| From e197bbbdcb8f03e07fe6823c8973fffa53ec659d Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Wed, 10 Dec 2025 00:17:45 +0000 Subject: [PATCH 3/3] eager-decode-then reorder received frames the immediate benefit of eager-decoding frames is that one can push PING frames to the top of the stack, thereby prioritizing its processing. --- lib/http/2/framer.rb | 18 ++++++++++++++++-- sig/framer.rbs | 5 ++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/http/2/framer.rb b/lib/http/2/framer.rb index b09c118..6ab0c11 100644 --- a/lib/http/2/framer.rb +++ b/lib/http/2/framer.rb @@ -119,6 +119,7 @@ def initialize(local_max_frame_size = DEFAULT_MAX_FRAME_SIZE, remote_max_frame_size = DEFAULT_MAX_FRAME_SIZE) @local_max_frame_size = local_max_frame_size @remote_max_frame_size = remote_max_frame_size + @frames = [] end # Generates common 9-byte frame header. @@ -371,6 +372,21 @@ def generate(frame) # # @param buf [Buffer] def parse(buf) + while (frame = decode_frame(buf)) + if frame[:type] == :ping + # PING responses SHOULD be given higher priority than any other frame. + @frames.unshift(frame) + else + @frames << frame + end + end + + @frames.shift + end + + private + + def decode_frame(buf) return if buf.size < 9 frame = read_common_header(buf) @@ -491,8 +507,6 @@ def parse(buf) frame end - private - def pack_error(error, buffer:) unless error.is_a? Integer error = DEFINED_ERRORS[error] diff --git a/sig/framer.rbs b/sig/framer.rbs index 5205990..ffc66b2 100644 --- a/sig/framer.rbs +++ b/sig/framer.rbs @@ -34,6 +34,7 @@ module HTTP2 @local_max_frame_size: Integer @remote_max_frame_size: Integer + @streams: Hash[Integer, Stream] attr_accessor local_max_frame_size: Integer @@ -47,12 +48,14 @@ module HTTP2 def generate: (frame) -> String - def parse: (String) -> frame? + def parse: (String buf) -> frame? private def initialize: (?Integer local_max_frame_size, ?Integer remote_max_frame_size) -> untyped + def decode_frame: (String buf) -> frame? + def pack_error: (Integer | Symbol error, buffer: String) -> String def unpack_error: (Integer) -> (Symbol | Integer)