From 74baa065be123d57a1aaad1d7dff8b7c7fabbd8a Mon Sep 17 00:00:00 2001 From: Martin Ek Date: Sat, 26 Sep 2026 20:59:13 -0700 Subject: [PATCH] Cancel HTTP/2 streams when requests are interrupted --- lib/async/http/protocol/http2/client.rb | 8 ++++ .../protocol/http2/request_cancellation.rb | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/async/http/protocol/http2/request_cancellation.rb diff --git a/lib/async/http/protocol/http2/client.rb b/lib/async/http/protocol/http2/client.rb index bf1ca4a8..6e359a02 100644 --- a/lib/async/http/protocol/http2/client.rb +++ b/lib/async/http/protocol/http2/client.rb @@ -42,10 +42,18 @@ def call(request) raise ::Protocol::HTTP::RefusedError, "Connection closed!" if self.closed? response = create_response + completed = false + write_request(response, request) read_response(response) + completed = true return response + ensure + # Cancel abandoned requests, including when the task is stopped while waiting for headers. + if response && !completed && !self.closed? && response.stream.active? + response.stream.send_reset_stream(::Protocol::HTTP2::Error::CANCEL) + end end # Write a request to the remote server via the given response stream. diff --git a/test/async/http/protocol/http2/request_cancellation.rb b/test/async/http/protocol/http2/request_cancellation.rb new file mode 100644 index 00000000..122f68e4 --- /dev/null +++ b/test/async/http/protocol/http2/request_cancellation.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "async/http/protocol/http2" +require "sus/fixtures/async/http" + +describe Async::HTTP::Protocol::HTTP2 do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:protocol) {subject} + + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Async::Task.current.sleep(10) if request.path == "/slow" + + Protocol::HTTP::Response[200, {}, ["OK"]] + end + end + + it "cancels a timed-out request and reuses the connection" do + connection = Async::HTTP::Protocol::HTTP2.client(client_endpoint.connect) + task = Async::Task.current + + expect do + task.with_timeout(0.1) do + connection.call(Protocol::HTTP::Request["GET", "/slow", scheme: "http", authority: "localhost"]) + end + end.to raise_exception(Async::TimeoutError) + + expect(connection.streams).to be(:empty?) + + response = connection.call(Protocol::HTTP::Request["GET", "/fast", scheme: "http", authority: "localhost"]) + expect(response).to be(:success?) + expect(response.read).to be == "OK" + ensure + response&.close + connection&.close + end +end