Repository: rack/rack (Ruby)

Issue:
Mock::Response returns empty body when body parts use same buffer object
It is reproduced with rack 2.2 and 3.0.beta1

Reproducing script
```ruby
require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
  gem "rack", '~> 3.0', '>= 3.0.4.2'
  # gem "rack", "2.2.4"
end

class FileIteratorWithBuffer
  def initialize(file)
    @file = file
  end

  def each
    buffer = String.new

    while @file.read(5, buffer)
      yield(buffer)
    end
  end
end

file = StringIO.new('Large large file content')
body = FileIteratorWithBuffer.new(file)

puts "Result: " + Rack::MockResponse.new(200, {}, body).body.inspect

# => Result: ""
```


Discussion:
If you call `yield(buffer.dup)` does it work as expected?
yes, Result: "Large large file content"
This is because `Rack::MockResponse` uses `Rack::RackResponse#buffered_body!`, which results in iterating over the body that is passed in, and creating a new array with each yielded string.

The weird thing is that `Rack::MockResponse#body` wants to return a string, not an array, so it makes no sense to do a `initial body -> intermediate array-> string` conversion instead of just going directly from `initial body -> string`.  I'll push a possible fix, which passes all specs.
Thanks a lot :rocket: 

By the way, in Rack::Files this  buffer approach can be used to save long strings allocations https://github.com/rack/rack/blob/main/lib/rack/files.rb#L171-L181
@jeremyevans Hi, can you reopen this issue? I've update rack version to 3.0.4.2, but provided reproducing script still not work. 
Agreed.  I'll reopen.  The underlying issue is that Rack::Response#buffered_body! keeps the buffer as separate strings in array.  It either needs to dup the strings (if not frozen) or keep the buffer as a single string in an array (or use another approach).
FWIW I have previously flagged this in https://github.com/rack/rack/issues/1722
@julik Is this still a problem for you?
Since the answer back then was "wontfix" I worked around it, but it might be that this issue (and the related PR) address my issue too 😄  sorry I wasn't clear. No action required.



Patch:
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index 4a6af373e..20a871595 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -342,6 +342,7 @@ def buffered_body!
       end
 
       def append(chunk)
+        chunk = chunk.dup unless chunk.frozen?
         @body << chunk
 
         unless chunked?


Test patch:
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 68227fa63..6a6957e57 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -604,6 +604,26 @@ def obj.each
     res.finish.last.must_equal ["Foo", "Bar"]
   end
 
+  it "handles string reuse in existing body when calling #write" do
+    body_class = Class.new do
+      def initialize(file)
+        @file = file
+      end
+
+      def each
+        buffer = String.new
+
+        while @file.read(5, buffer)
+          yield(buffer)
+        end
+      end
+    end
+    body = body_class.new(StringIO.new('Large large file content'))
+    res = Rack::Response.new(body)
+    res.write(" written")
+    res.finish.last.must_equal ["Large", " larg", "e fil", "e con", "tent", " written"]
+  end
+
   it "calls close on #body" do
     res = Rack::Response.new
     res.body = StringIO.new