Skip to content

Commit 1c5b346

Browse files
Fix HTTP/2 response leak.
1 parent 5004f0a commit 1c5b346

File tree

3 files changed

+67
-0
lines changed

3 files changed

+67
-0
lines changed

lib/async/http/protocol/http2/server.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def each(task: Task.current)
6060
@requests&.async do |task, request|
6161
task.annotate("Incoming request: #{request.method} #{request.path.inspect}.")
6262

63+
response = nil
64+
6365
task.defer_stop do
6466
response = yield(request)
6567
rescue
@@ -69,6 +71,8 @@ def each(task: Task.current)
6971
raise
7072
else
7173
request.send_response(response)
74+
ensure
75+
response&.close
7276
end
7377
end
7478

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Fix response body leak in HTTP/2 server when stream is reset before `send_response` completes (e.g. client-side gRPC cancellation). The response body's `close` was never called, leaking any resources tied to body lifecycle (such as `rack.response_finished` callbacks and utilization metrics).
6+
37
## v0.94.1
48

59
- Fix `defer_stop` usage in `HTTP1::Server`, improving server graceful shutdown behavior.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2026, by Samuel Williams.
5+
6+
require "async/http/protocol/http2"
7+
require "sus/fixtures/async/http"
8+
require "protocol/http/body/wrapper"
9+
10+
describe Async::HTTP::Protocol::HTTP2 do
11+
with "response body close on stream error" do
12+
include Sus::Fixtures::Async::HTTP::ServerContext
13+
let(:protocol) {subject}
14+
15+
let(:body_closed) {Async::Variable.new}
16+
17+
let(:app) do
18+
variable = body_closed
19+
20+
Protocol::HTTP::Middleware.for do |request|
21+
inner_body = Protocol::HTTP::Body::Buffered.new(["Hello World"])
22+
23+
tracking_body = Class.new(Protocol::HTTP::Body::Wrapper) do
24+
define_method(:close) do |error = nil|
25+
super(error)
26+
variable.value = true
27+
end
28+
end.new(inner_body)
29+
30+
if request.respond_to?(:stream) && request.stream
31+
Async do
32+
sleep(0.01)
33+
request.stream.send_reset_stream(Protocol::HTTP2::NO_ERROR)
34+
end
35+
36+
sleep(0.05)
37+
end
38+
39+
Protocol::HTTP::Response[200, {}, tracking_body]
40+
end
41+
end
42+
43+
it "closes the response body when the stream is reset before sending" do
44+
expect do
45+
response = client.get("/")
46+
end.to raise_exception(Exception)
47+
48+
# Wait up to 1 second for the body to be closed. Without the fix,
49+
# close is never called and this times out → test failure.
50+
result = Async::Task.current.with_timeout(1.0) do
51+
body_closed.wait
52+
rescue Async::TimeoutError
53+
nil
54+
end
55+
56+
expect(result).to be == true
57+
end
58+
end
59+
end

0 commit comments

Comments
 (0)