Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions lib/async/http/protocol/http2/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,9 @@ def closed(error)
@response = nil
end

if error
unless @ready.resolved?
error ||= EOFError.new("Stream closed before response headers were received!")
@ready.reject(error)
else
@ready.resolve(nil)
end
end
end
Expand Down
11 changes: 4 additions & 7 deletions lib/async/http/protocol/http2/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,7 @@ def send_data(...)
# - A frame is sent which causes this stream to enter the closed state. This method will be invoked from that task.
# While the input stream is relatively straight forward, the output stream can trigger the second case above
def closed(error)
orderly_reset = error.is_a?(::Protocol::HTTP2::StreamError) &&
error.code == ::Protocol::HTTP2::Error::NO_ERROR

if orderly_reset
if error.is_a?(::Protocol::HTTP2::StreamError) && error.code == ::Protocol::HTTP2::Error::NO_ERROR
error = nil
end

Expand All @@ -227,10 +224,10 @@ def closed(error)
if output = @output
@output = nil

if orderly_reset
output.close_stream
else
if error
output.stop(error)
else
output.close_stream
end
end

Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Handle `RST_STREAM(NO_ERROR)` as an orderly HTTP/2 stream closure while still failing requests whose streams close before any response headers are received.

## v0.102.0

- Requests assigned to an HTTP/2 connection which has already closed are refused before being written, allowing them to be retried safely.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@
end.wait
end

it "raises EOFError when the stream closes before receiving headers" do
Async do
client_connection = Async::HTTP::Protocol::HTTP2::Client.new(client_stream)
client_connection.open!

response = client_connection.create_response
response.stream.close!

expect do
client_connection.read_response(response)
end.to raise_exception(EOFError, message: be =~ /Stream closed before response headers/)
end.wait
end

it "does not raise error when connection closes without active streams" do
Async do |task|
# Create client connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
require "protocol/http/body/wrapper"

describe Async::HTTP::Protocol::HTTP2 do
with "response body close on stream error" do
with "response body close on stream reset" do
include Sus::Fixtures::Async::HTTP::ServerContext
let(:protocol) {subject}

Expand Down
42 changes: 42 additions & 0 deletions test/async/http/protocol/http2/stream.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "async/http/protocol/http2/stream"

describe Async::HTTP::Protocol::HTTP2::Stream do
let(:output) do
Class.new do
def close_stream
end

def stop(error)
end
end.new
end

let(:stream) do
Class.new(subject) do
def initialize(output)
@input = nil
@output = output
@pool = nil
@connection = nil
end
end.new(output)
end

it "closes the output stream on an orderly closure" do
expect(output).to receive(:close_stream)

stream.closed(nil)
end

it "stops the output stream on an error" do
error = RuntimeError.new("Stream failed!")
expect(output).to receive(:stop).with(error)

stream.closed(error)
end
end
Loading