From 2a14008d3f35157187b92ff783b0eb3d6a062d0c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 1 Sep 2026 22:57:15 +1200 Subject: [PATCH 1/4] Treat NO_ERROR resets as orderly closure Assisted-By: devx/618580b0-d55f-4c2e-95b6-87e648f60543 --- lib/protocol/http2/stream.rb | 15 +++++++-------- releases.md | 5 +++++ test/protocol/http2/connection.rb | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/protocol/http2/stream.rb b/lib/protocol/http2/stream.rb index 3ddbcdb..a862190 100644 --- a/lib/protocol/http2/stream.rb +++ b/lib/protocol/http2/stream.rb @@ -239,22 +239,21 @@ def open! return self end - # The stream has been closed. If closed due to a stream reset, the error will be set. + # The stream has been closed. If closed due to a stream reset with a + # non-zero error code, the error will be set. def closed(error = nil) end # Transition the stream into the closed state. - # @parameter error_code [Integer] the error code if the stream was closed due to a stream reset. + # @parameter error_code [Integer | Nil] The error code if the stream was closed due to a stream reset. def close!(error_code = nil) @state = :closed @connection.delete(@id) - if error_code - if error_code == REFUSED_STREAM - error = ::Protocol::HTTP::RefusedError.new("Stream refused.") - else - error = StreamError.for(error_code) - end + if error_code == REFUSED_STREAM + error = ::Protocol::HTTP::RefusedError.new("Stream refused.") + elsif error_code && error_code != NO_ERROR + error = StreamError.for(error_code) end self.closed(error) diff --git a/releases.md b/releases.md index 4ce332c..6d45f07 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,10 @@ # Releases +## Unreleased + + - Treat `RST_STREAM(NO_ERROR)` as an orderly stream closure rather than + constructing a `StreamError`. + ## v0.27.0 - On a graceful `GOAWAY` (error code `0`), keep the connection open until the streams the remote peer accepted have completed, instead of closing it immediately and failing those requests with `EOFError`. diff --git a/test/protocol/http2/connection.rb b/test/protocol/http2/connection.rb index 23b0465..cc9b5d1 100644 --- a/test/protocol/http2/connection.rb +++ b/test/protocol/http2/connection.rb @@ -448,6 +448,24 @@ def closed(error) expect(stream.error).to be_a(Protocol::HTTP::RefusedError) end + it "closes stream without an error on NO_ERROR" do + stream = client.create_stream do |connection, id| + stream_class.create(connection, id) + end + stream.send_headers(request_headers, Protocol::HTTP2::END_STREAM) + + # Establish request stream on server: + server.read_frame + + # Server closes the stream without an error: + server.streams[stream.id].send_reset_stream(Protocol::HTTP2::NO_ERROR) + + client.read_frame + + expect(stream.state).to be == :closed + expect(stream.error).to be_nil + end + it "closes unprocessed streams with RefusedError on graceful GOAWAY" do stream.send_headers(request_headers, Protocol::HTTP2::END_STREAM) From a60e035cca0ef669b0f1a4cabfaf7c21b2f25569 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 2 Sep 2026 09:28:09 +1200 Subject: [PATCH 2/4] Avoid wrapping lifecycle comment Assisted-By: devx/618580b0-d55f-4c2e-95b6-87e648f60543 --- lib/protocol/http2/stream.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/protocol/http2/stream.rb b/lib/protocol/http2/stream.rb index a862190..b8330b3 100644 --- a/lib/protocol/http2/stream.rb +++ b/lib/protocol/http2/stream.rb @@ -239,8 +239,7 @@ def open! return self end - # The stream has been closed. If closed due to a stream reset with a - # non-zero error code, the error will be set. + # The stream has been closed. If closed due to a stream reset with a non-zero error code, the error will be set. def closed(error = nil) end From 3f3e18d8aa83492bc83f70cbfad393aafc1afa79 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 2 Sep 2026 09:28:39 +1200 Subject: [PATCH 3/4] Explain NO_ERROR closure handling Assisted-By: devx/618580b0-d55f-4c2e-95b6-87e648f60543 --- lib/protocol/http2/stream.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/protocol/http2/stream.rb b/lib/protocol/http2/stream.rb index b8330b3..6224a93 100644 --- a/lib/protocol/http2/stream.rb +++ b/lib/protocol/http2/stream.rb @@ -249,6 +249,7 @@ def close!(error_code = nil) @state = :closed @connection.delete(@id) + # `NO_ERROR` represents an orderly stream closure, so it is passed to `closed` as `nil` rather than converted to an exception. if error_code == REFUSED_STREAM error = ::Protocol::HTTP::RefusedError.new("Stream refused.") elsif error_code && error_code != NO_ERROR From 8d2c7f56fbae0f9374a0543f6a13825881b1ca46 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 2 Sep 2026 09:29:19 +1200 Subject: [PATCH 4/4] Avoid wrapping release note Assisted-By: devx/618580b0-d55f-4c2e-95b6-87e648f60543 --- releases.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/releases.md b/releases.md index 6d45f07..6ef06e6 100644 --- a/releases.md +++ b/releases.md @@ -2,8 +2,7 @@ ## Unreleased - - Treat `RST_STREAM(NO_ERROR)` as an orderly stream closure rather than - constructing a `StreamError`. + - Treat `RST_STREAM(NO_ERROR)` as an orderly stream closure rather than constructing a `StreamError`. ## v0.27.0