From d97698b1c4e3f04e071666b0da8f4a59070aa6ae Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 5 Feb 2026 08:25:49 +1300 Subject: [PATCH 1/3] Fix streaming body close. --- lib/protocol/rack/body/enumerable.rb | 12 +++++++----- lib/protocol/rack/body/streaming.rb | 19 ++++++++++++++++++- releases.md | 4 ++++ test/protocol/rack/body/streaming.rb | 25 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/lib/protocol/rack/body/enumerable.rb b/lib/protocol/rack/body/enumerable.rb index a525cdc..80b41a8 100644 --- a/lib/protocol/rack/body/enumerable.rb +++ b/lib/protocol/rack/body/enumerable.rb @@ -69,13 +69,15 @@ def ready? # # @parameter error [Exception] Optional error that occurred during processing. def close(error = nil) - if @body and @body.respond_to?(:close) - @body.close - end - - @body = nil @chunks = nil + if body = @body + @body = nil + if body.respond_to?(:close) + body.close + end + end + super end diff --git a/lib/protocol/rack/body/streaming.rb b/lib/protocol/rack/body/streaming.rb index 5e695ae..fc0279d 100644 --- a/lib/protocol/rack/body/streaming.rb +++ b/lib/protocol/rack/body/streaming.rb @@ -8,7 +8,24 @@ module Protocol module Rack module Body - Streaming = ::Protocol::HTTP::Body::Streamable::ResponseBody + class Streaming < ::Protocol::HTTP::Body::Streamable::ResponseBody + def initialize(body, input = nil) + @body = body + + super + end + + def close(error = nil) + if body = @body + @body = nil + if body.respond_to?(:close) + body.close + end + end + + super + end + end end end end diff --git a/releases.md b/releases.md index 4104ca1..3bd77ee 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Fix missing `body#close` for streaming bodies. + ## v0.21.0 - For the purpose of constructing the rack request environment, trailers are ignored. diff --git a/test/protocol/rack/body/streaming.rb b/test/protocol/rack/body/streaming.rb index 814e427..c757642 100644 --- a/test/protocol/rack/body/streaming.rb +++ b/test/protocol/rack/body/streaming.rb @@ -50,4 +50,29 @@ expect(body.read).to be == "Hello" end end + + with "#close" do + it "closes the wrapped body if it responds to close" do + close_called = false + wrapped_body = Object.new + wrapped_body.define_singleton_method(:close) do + close_called = true + end + wrapped_body.define_singleton_method(:call) do |stream| + stream.write("Hello") + end + + body = subject.new(wrapped_body) + body.close + + expect(close_called).to be == true + end + + it "does not fail if wrapped body does not respond to close" do + wrapped_body = proc { |stream| stream.write("Hello") } + + body = subject.new(wrapped_body) + body.close + end + end end From e6ea7795980ef2ff3db6868a207794283edf3b83 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 5 Feb 2026 08:36:30 +1300 Subject: [PATCH 2/3] Documentation coverage. --- lib/protocol/rack/body/streaming.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/protocol/rack/body/streaming.rb b/lib/protocol/rack/body/streaming.rb index fc0279d..bfc4d06 100644 --- a/lib/protocol/rack/body/streaming.rb +++ b/lib/protocol/rack/body/streaming.rb @@ -8,13 +8,26 @@ module Protocol module Rack module Body + # Wraps a Rack streaming response body. + # The body must be callable and accept a stream argument. + # This is typically used for Rack hijack responses or bodies wrapped in `Rack::BodyProxy`. + # When closed, this class ensures the wrapped body's `close` method is called if it exists. class Streaming < ::Protocol::HTTP::Body::Streamable::ResponseBody + # Initialize the streaming body wrapper. + # + # @parameter body [Object] A callable object that accepts a stream argument, such as a Proc or an object that responds to `call`. May optionally respond to `close` for cleanup (e.g., `Rack::BodyProxy`). + # @parameter input [Protocol::HTTP::Body::Readable | Nil] Optional input body for bi-directional streaming. def initialize(body, input = nil) @body = body super end + # Close the streaming body and clean up resources. + # If the wrapped body responds to `close`, it will be called to allow proper cleanup. + # This ensures that `Rack::BodyProxy` cleanup callbacks are invoked correctly. + # + # @parameter error [Exception | Nil] Optional error that caused the stream to close. def close(error = nil) if body = @body @body = nil From 552670eaff194c46895be63031dfab08038fd5c6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 5 Feb 2026 08:40:15 +1300 Subject: [PATCH 3/3] RuboCop. --- test/protocol/rack/body/streaming.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/protocol/rack/body/streaming.rb b/test/protocol/rack/body/streaming.rb index c757642..67923b3 100644 --- a/test/protocol/rack/body/streaming.rb +++ b/test/protocol/rack/body/streaming.rb @@ -69,7 +69,7 @@ end it "does not fail if wrapped body does not respond to close" do - wrapped_body = proc { |stream| stream.write("Hello") } + wrapped_body = proc{|stream| stream.write("Hello")} body = subject.new(wrapped_body) body.close