From eb9fb7c8f2beaab7a3223f0a1659f9006e495215 Mon Sep 17 00:00:00 2001 From: Andrew Fader Date: Fri, 30 Oct 2026 20:09:26 -0400 Subject: [PATCH] Don't request a window update for a closed HTTP/2 stream. --- lib/async/http/protocol/http2/stream.rb | 5 +++ .../http/protocol/http2/graceful_goaway.rb | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/lib/async/http/protocol/http2/stream.rb b/lib/async/http/protocol/http2/stream.rb index 2ca4207c..6155e3e8 100644 --- a/lib/async/http/protocol/http2/stream.rb +++ b/lib/async/http/protocol/http2/stream.rb @@ -111,6 +111,11 @@ def update_local_window(frame) # request_window_update end + # A closed stream receives no more data, so there is no window to extend. This happens when the application reads a body which was fully buffered before the connection closed, e.g. after a graceful GOAWAY. + def request_window_update + super unless closed? + end + # Process an incoming DATA frame and write it to the input body. # @parameter frame [Protocol::HTTP2::DataFrame] The data frame to process. # @returns [String] The unpacked data. diff --git a/test/async/http/protocol/http2/graceful_goaway.rb b/test/async/http/protocol/http2/graceful_goaway.rb index c3d76b00..7a163349 100644 --- a/test/async/http/protocol/http2/graceful_goaway.rb +++ b/test/async/http/protocol/http2/graceful_goaway.rb @@ -11,6 +11,7 @@ require "sus/fixtures/async/scheduler_context" require "async/queue" +require "protocol/http/body/writable" require "io/stream" require "socket" @@ -117,6 +118,46 @@ def handle_connection(socket, index, processed) end end + with "a response body which is still buffered when the GOAWAY closes the connection" do + let(:sockets) {::Socket.pair(::Socket::AF_UNIX, ::Socket::SOCK_STREAM)} + + def after(error = nil) + sockets.each{|socket| socket.close unless socket.closed?} + + super + end + + it "can read the whole body" do + chunk = "x" * 100_000 + + server_task = Async do + server = subject.server(sockets.last) + + server.each do + body = ::Protocol::HTTP::Body::Writable.new + + Async do + 60.times{body.write(chunk)} + server.write_frame(goaway_frame(1)) + body.close_write + end + + ::Protocol::HTTP::Response[200, {}, body] + end + end + + client = subject.client(sockets.first) + response = client.call(::Protocol::HTTP::Request["GET", "/", authority: "localhost", scheme: "http"]) + + # The stream completes, so the connection closes, before the body has been read: + Async::Task.current.sleep(0.01) until client.closed? + + expect(response.read.bytesize).to be == 6_000_000 + ensure + server_task&.stop + end + end + with "a connection which received a GOAWAY" do let(:sockets) {::Socket.pair(::Socket::AF_UNIX, ::Socket::SOCK_STREAM)} let(:connection) {Async::HTTP::Protocol::HTTP2::Client.new(IO::Stream(sockets.first))}