From a2d8738289b1017a8df4dd4a47d72e2342c19559 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 31 Aug 2026 15:30:15 +1200 Subject: [PATCH] Refuse requests assigned to closed HTTP/2 connections Assisted-By: devx/618580b0-d55f-4c2e-95b6-87e648f60543 --- lib/async/http/protocol/http2/client.rb | 3 ++- releases.md | 1 + .../http2/connection_close_with_active_streams.rb | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/async/http/protocol/http2/client.rb b/lib/async/http/protocol/http2/client.rb index b1c1dd4a..f52ababf 100644 --- a/lib/async/http/protocol/http2/client.rb +++ b/lib/async/http/protocol/http2/client.rb @@ -37,7 +37,8 @@ def call(request) # The remote peer has sent a GOAWAY frame, so it will not process any new streams on this connection. The request has not been sent yet, so it is safe to retry it on a new connection, even if it is not idempotent. This is checked first, because a connection with no streams left to drain is closed by the GOAWAY itself. raise ::Protocol::HTTP::RefusedError, "Connection is going away!" if self.goaway_received? - raise ::Protocol::HTTP2::Error, "Connection closed!" if self.closed? + # The connection can close after being acquired from the pool. No stream has been created yet, so the request has not been written and is safe to retry. + raise ::Protocol::HTTP::RefusedError, "Connection closed!" if self.closed? response = create_response write_request(response, request) diff --git a/releases.md b/releases.md index 65737862..509a2681 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,7 @@ ## Unreleased + - Requests assigned to an HTTP/2 connection which has already closed are refused before being written, allowing them to be retried safely. - HTTP/2 connections which received a graceful `GOAWAY` are removed from availability immediately, but remain in the pool until the server has finished answering the streams it accepted. The connection is closed after its final user releases it, so those requests no longer fail with `EOFError: Connection closed with N active stream(s)!`. ## v0.101.0 diff --git a/test/async/http/protocol/http2/connection_close_with_active_streams.rb b/test/async/http/protocol/http2/connection_close_with_active_streams.rb index dcc24b9d..33701380 100644 --- a/test/async/http/protocol/http2/connection_close_with_active_streams.rb +++ b/test/async/http/protocol/http2/connection_close_with_active_streams.rb @@ -85,5 +85,17 @@ expect(client_connection).to be(:closed?) end.wait end + + it "refuses a request when the connection is already closed" do + Async do + client_connection = Async::HTTP::Protocol::HTTP2::Client.new(client_stream) + client_connection.open! + client_connection.close + + expect do + client_connection.call(Protocol::HTTP::Request["POST", "/", {}, ["Hello"]]) + end.to raise_exception(Protocol::HTTP::RefusedError) + end.wait + end end end