diff --git a/lib/http/2/connection.rb b/lib/http/2/connection.rb index 6e74bc9..be77985 100644 --- a/lib/http/2/connection.rb +++ b/lib/http/2/connection.rb @@ -295,7 +295,12 @@ def receive(data) # PUSH_PROMISE and CONTINUATION frames MUST be minimally # processed to ensure a consistent compression state decode_headers(frame) - return if @state == :closed + # A GOAWAY moves the connection to :closed, but streams the peer + # promised to finish (id <= last_stream_id, already in @streams) can + # still complete (RFC 9113 Section 6.8). Only discard HEADERS that + # would open a NEW stream; a known stream's response must be + # delivered (its DATA frames already are - see the branch below). + return if @state == :closed && !@streams.key?(stream_id) stream = @streams[stream_id] if stream.nil? diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 1d40f11..02722e5 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -309,6 +309,36 @@ client << f.generate(push_promise_frame) expect(client.active_stream_count).to eq 1 end + + it "should deliver response headers for a stream the GOAWAY promised to finish" do + stream = client.new_stream # id 1 + stream.send headers_frame + + received = nil + stream.on(:headers) { |h| received = h } + + # GOAWAY (last_stream: 2) covers this stream (id 1), so the server + # promised to finish it: its response, arriving after the GOAWAY, must + # still be delivered rather than dropped once the connection is :closed. + client << f.generate(goaway_frame) + client << f.generate(headers_frame.merge(payload: Compressor.new.encode(RESPONSE_HEADERS))) + + expect(received).to eq(RESPONSE_HEADERS) + end + + it "should drop response headers for a stream the GOAWAY refused" do + client << f.generate(goaway_frame) # last_stream: 2 + + opened = false + client.on(:stream) { opened = true } + + # A stream beyond last_stream_id (id 5) was refused and never processed; + # its (unexpected) headers must not open a stream on the closed connection. + client << f.generate(headers_frame.merge(stream: 5, payload: Compressor.new.encode(RESPONSE_HEADERS))) + + expect(opened).to be(false) + expect(client.active_stream_count).to eq 0 + end end context "framing" do