From eee18ab2078da987aaaba74a61d55379e4d0988a Mon Sep 17 00:00:00 2001 From: Andrew Fader Date: Fri, 23 Oct 2026 03:02:55 -0400 Subject: [PATCH] Release a partially read HTTP/1 request body after the response. Signed-off-by: Andrew Fader --- lib/async/http/protocol/http1/finishable.rb | 13 ++++-- test/async/http/protocol/http1/finishable.rb | 48 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 test/async/http/protocol/http1/finishable.rb diff --git a/lib/async/http/protocol/http1/finishable.rb b/lib/async/http/protocol/http1/finishable.rb index 6d02ba46..53123160 100644 --- a/lib/async/http/protocol/http1/finishable.rb +++ b/lib/async/http/protocol/http1/finishable.rb @@ -11,7 +11,7 @@ module Async module HTTP module Protocol module HTTP1 - # Keeps track of whether a body is being read, and if so, waits for it to be closed. + # Keeps track of whether a body is being read, and releases it once the response has been sent. class Finishable < ::Protocol::HTTP::Body::Wrapper # Initialize the finishable wrapper. # @parameter body [Protocol::HTTP::Body::Readable] The body to wrap. @@ -47,11 +47,18 @@ def close(error = nil) end end - # Wait for the body to be fully consumed or discard it. + # Discard or close the body once the response has been sent. # @parameter persistent [Boolean] Whether the connection will be reused. def wait(persistent = true) if @reading - @closed.wait + # The response has been sent, so release a body the application stopped reading: + unless @closed.resolved? + begin + self.discard if persistent + ensure + self.close + end + end elsif persistent # If the connection can be reused, let's gracefully discard the body: self.discard diff --git a/test/async/http/protocol/http1/finishable.rb b/test/async/http/protocol/http1/finishable.rb new file mode 100644 index 00000000..5aa213a7 --- /dev/null +++ b/test/async/http/protocol/http1/finishable.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Andrew Fader. + +require "async/http/protocol/http1/finishable" + +require "protocol/http/body/buffered" +require "sus/fixtures/async/reactor_context" + +describe Async::HTTP::Protocol::HTTP1::Finishable do + include Sus::Fixtures::Async::ReactorContext + + let(:source) {Protocol::HTTP::Body::Buffered.new(["Hello", " ", "World"])} + let(:finishable) {subject.new(source)} + + with "a partially read body" do + it "discards the rest when the connection is persistent" do + expect(finishable.read).to be == "Hello" + + Async::Task.current.with_timeout(1) do + finishable.wait(true) + end + + expect(source.read).to be_nil + end + + it "closes it when the connection is not persistent" do + expect(finishable.read).to be == "Hello" + + expect(source).to receive(:close) + + Async::Task.current.with_timeout(1) do + finishable.wait(false) + end + end + end + + with "a fully read body" do + it "returns once it has been closed" do + expect(finishable.join).to be == "Hello World" + + Async::Task.current.with_timeout(1) do + finishable.wait(true) + end + end + end +end