From 8881c7dfe5e2b330de5f01ba6a296f1459d3cd47 Mon Sep 17 00:00:00 2001 From: thomas morgan Date: Sun, 7 Dec 2025 11:05:26 -0700 Subject: [PATCH 1/2] Allow multiple GOAWAY frames Per rfc9113, section 6.8, GOAWAY may be resent: - if there is a delay between an earlier GOAWAY and closing the socket - to send different values > A GOAWAY frame might not immediately precede closing of the connection; a receiver of a GOAWAY that has no more use for the connection SHOULD still send a GOAWAY frame before terminating the connection. > An endpoint MAY send multiple GOAWAY frames if circumstances change. --- lib/http/2/connection.rb | 3 ++- spec/shared_examples/connection.rb | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/http/2/connection.rb b/lib/http/2/connection.rb index 201ce87..d3cf180 100644 --- a/lib/http/2/connection.rb +++ b/lib/http/2/connection.rb @@ -525,7 +525,8 @@ def connection_management(frame) when :closed case frame_type when :goaway - connection_error + # 6.8. GOAWAY + # An endpoint MAY send multiple GOAWAY frames if circumstances change. when :ping ping_management(frame) else diff --git a/spec/shared_examples/connection.rb b/spec/shared_examples/connection.rb index b4c37dc..a2e9f20 100644 --- a/spec/shared_examples/connection.rb +++ b/spec/shared_examples/connection.rb @@ -173,11 +173,13 @@ expect { conn << f.generate(ping_frame) }.not_to raise_error(ProtocolError) end - it "should respond with protocol error when receiving goaway" do + it "should ignore additional goaway" do + expect { conn << f.generate(settings_frame) }.not_to raise_error(ProtocolError) + conn.goaway expect(conn).to be_closed - expect { conn << f.generate(goaway_frame) }.to raise_error(ProtocolError) + expect { conn << f.generate(goaway_frame) }.not_to raise_error(ProtocolError) end it "should raise error on frame for invalid stream ID" do From 14fbf819ce31d8a89f391e6c694711c92bcf6cbc Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 9 Dec 2025 11:55:40 +0000 Subject: [PATCH 2/2] autofix lint check failures --- lib/http/2/extensions.rb | 2 +- lib/http/2/framer.rb | 4 ++-- lib/http/2/header/encoding_context.rb | 29 +++++++++++++++------------ lib/http/2/header/huffman.rb | 2 +- sig/header/encoding_context.rbs | 4 +++- spec/connection_spec.rb | 1 + spec/emitter_spec.rb | 1 + spec/stream_spec.rb | 1 + tasks/generate_huffman_table.rb | 6 +++--- 9 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/http/2/extensions.rb b/lib/http/2/extensions.rb index df0a0cc..8e840b6 100644 --- a/lib/http/2/extensions.rb +++ b/lib/http/2/extensions.rb @@ -25,7 +25,7 @@ def append_str(str, data) def read_str(str, n) return "".b if n == 0 - chunk = str.byteslice(0..n - 1) + chunk = str.byteslice(0..(n - 1)) remaining = str.byteslice(n..-1) remaining ? str.replace(remaining) : str.clear chunk diff --git a/lib/http/2/framer.rb b/lib/http/2/framer.rb index 2a36425..b09c118 100644 --- a/lib/http/2/framer.rb +++ b/lib/http/2/framer.rb @@ -185,7 +185,7 @@ def read_common_header(buf) { type: type, flags: FRAME_FLAGS[type].filter_map do |name, pos| - name if flags.anybits?((1 << pos)) + name if flags.anybits?(1 << pos) end, length: length, stream: stream & RBIT @@ -359,7 +359,7 @@ def generate(frame) # Padding: Padding octets that contain no application semantic value. # Padding octets MUST be set to zero when sending and ignored when # receiving. - append_str(bytes, ("\0" * padlen)) + append_str(bytes, "\0" * padlen) end frame[:length] = length diff --git a/lib/http/2/header/encoding_context.rb b/lib/http/2/header/encoding_context.rb index 2679e65..d59ffac 100644 --- a/lib/http/2/header/encoding_context.rb +++ b/lib/http/2/header/encoding_context.rb @@ -211,7 +211,7 @@ def process(cmd) emit = [name, value] # add to table - if type == :incremental && size_check(name.bytesize + value.bytesize + 32) + if type == :incremental && size_check?(name.bytesize + value.bytesize + 32) @table.unshift(emit) @current_table_size += name.bytesize + value.bytesize + 32 @_table_updated = true @@ -302,7 +302,7 @@ def addcmd(field, value) # When the size is reduced, some headers might be evicted. def table_size=(size) @limit = size - size_check(0) + resize_table(0) end def listen_on_table @@ -313,22 +313,25 @@ def listen_on_table private + def resize_table(cmdsize) + return if @table.empty? + + while @current_table_size + cmdsize > @limit + + name, value = @table.pop + @current_table_size -= name.bytesize + value.bytesize + 32 + break if @table.empty? + + end + end + # To keep the dynamic table size lower than or equal to @limit, # remove one or more entries at the end of the dynamic table. # # @param cmdsize [Integer] # @return [Boolean] whether +cmd+ fits in the dynamic table. - def size_check(cmdsize) - unless @table.empty? - while @current_table_size + cmdsize > @limit - - name, value = @table.pop - @current_table_size -= name.bytesize + value.bytesize + 32 - break if @table.empty? - - end - end - + def size_check?(cmdsize) + resize_table(cmdsize) cmdsize <= @limit end end diff --git a/lib/http/2/header/huffman.rb b/lib/http/2/header/huffman.rb index 37c4f4e..1036174 100644 --- a/lib/http/2/header/huffman.rb +++ b/lib/http/2/header/huffman.rb @@ -29,7 +29,7 @@ module Huffman def encode(str, buffer = "".b) bitstring = String.new("", encoding: Encoding::BINARY, capacity: (str.bytesize * 30) + ((8 - str.size) % 8)) str.each_byte { |chr| append_str(bitstring, ENCODE_TABLE[chr]) } - append_str(bitstring, ("1" * ((8 - bitstring.size) % 8))) + append_str(bitstring, "1" * ((8 - bitstring.size) % 8)) pack([bitstring], "B*", buffer: buffer) end diff --git a/sig/header/encoding_context.rbs b/sig/header/encoding_context.rbs index e20fe6d..1085d4b 100644 --- a/sig/header/encoding_context.rbs +++ b/sig/header/encoding_context.rbs @@ -46,7 +46,9 @@ module HTTP2 def add_to_table: (string name, string value) -> void - def size_check: (Integer cmdsize) -> bool + def resize_table: (Integer cmdsize) -> void + + def size_check?: (Integer cmdsize) -> bool end end end diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb index e238aa2..7c358b1 100644 --- a/spec/connection_spec.rb +++ b/spec/connection_spec.rb @@ -4,6 +4,7 @@ RSpec.describe HTTP2::Connection do include FrameHelpers + let(:conn) { Client.new } let(:f) { Framer.new } diff --git a/spec/emitter_spec.rb b/spec/emitter_spec.rb index 5b32e0d..e121a7c 100644 --- a/spec/emitter_spec.rb +++ b/spec/emitter_spec.rb @@ -5,6 +5,7 @@ RSpec.describe HTTP2::Emitter do class Worker include Emitter + def initialize @listeners = Hash.new { |hash, key| hash[key] = [] } end diff --git a/spec/stream_spec.rb b/spec/stream_spec.rb index c30d237..692d36f 100644 --- a/spec/stream_spec.rb +++ b/spec/stream_spec.rb @@ -4,6 +4,7 @@ RSpec.describe HTTP2::Stream do include FrameHelpers + let(:f) { Framer.new } let(:client) { Client.new } let(:stream) { client.new_stream } diff --git a/tasks/generate_huffman_table.rb b/tasks/generate_huffman_table.rb index cc43f90..783b9b1 100644 --- a/tasks/generate_huffman_table.rb +++ b/tasks/generate_huffman_table.rb @@ -28,7 +28,7 @@ def add(code, len, chr) if len.zero? @emit = chr else - bit = code.nobits?((1 << (len - 1))) ? 0 : 1 + bit = code.nobits?(1 << (len - 1)) ? 0 : 1 node = @next[bit] ||= Node.new(@depth + 1) node.add(code, len - 1, chr) end @@ -70,7 +70,7 @@ def self.generate_machine n = node emit = "".b (BITS_AT_ONCE - 1).downto(0) do |i| - bit = input.nobits?((1 << i)) ? 0 : 1 + bit = input.nobits?(1 << i) ? 0 : 1 n = n.next[bit] next unless n.emit @@ -123,7 +123,7 @@ class Huffman id.times do |i| n = id_state[i] f.print " [" - string = Array.new((1 << 4)) do |t| + string = Array.new(1 << 4) do |t| transition = n.transitions.fetch(t) emit = transition.emit unless emit == EOS