From fa71ce764a3cf38b4c4f20489ec0b5ee3ca6db84 Mon Sep 17 00:00:00 2001 From: hikmetba-bit Date: Sat, 19 Sep 2026 17:00:17 +0300 Subject: [PATCH] Allow Response::Body#each to take a buffer_size Body#each always read in HTTP::Connection::BUFFER_SIZE (16KB) chunks with no way to override it, which meant streaming large responses (e.g. multi-gigabyte files via ActionController::Live) required many more reads than necessary. Add an optional buffer_size: keyword to #each, defaulting to the existing Connection::BUFFER_SIZE so behavior is unchanged unless a caller opts in, per the implementation @ixti sketched in #782. Fixes #782. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 7 +++++++ lib/http/response/body.rb | 8 ++++++-- sig/http.rbs | 4 ++-- test/http/response/body_test.rb | 28 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954aa1ae..6ab9fdac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `HTTP::Response::Body#each` now accepts a `buffer_size:` keyword argument + to control the chunk size used when streaming the body, instead of always + using `HTTP::Connection::BUFFER_SIZE` (16KB). ([#782]) + ### Fixed - Building a default `Host` header now raises `HTTP::RequestError` when the @@ -293,6 +299,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Request` and `Response`. Use `request.headers["..."]` and `response.headers["..."]` instead ([#537]) +[#782]: https://github.com/httprb/http/issues/782 [#270]: https://github.com/httprb/http/issues/270 [#223]: https://github.com/httprb/http/issues/223 [#358]: https://github.com/httprb/http/issues/358 diff --git a/lib/http/response/body.rb b/lib/http/response/body.rb index b31eebf2..ebe47dd2 100644 --- a/lib/http/response/body.rb +++ b/lib/http/response/body.rb @@ -67,13 +67,17 @@ def readpartial(*) # @example # body.each { |chunk| puts chunk } # + # @example + # body.each(buffer_size: 1.megabyte) { |chunk| puts chunk } + # + # @param buffer_size [Integer] the maximum size of each yielded chunk # @yield [chunk] Passes each chunk to the block # @yieldparam chunk [String] # @return [void] # @api public - def each + def each(buffer_size: Connection::BUFFER_SIZE) loop do - yield readpartial + yield readpartial(buffer_size) end rescue EOFError # rubocop:disable Lint/SuppressedException end diff --git a/sig/http.rbs b/sig/http.rbs index 9b1c4a69..64aebf76 100644 --- a/sig/http.rbs +++ b/sig/http.rbs @@ -1034,8 +1034,8 @@ module HTTP def initialize: (untyped stream, ?encoding: String | Encoding) -> void def readpartial: (?Integer size, ?String? outbuf) -> String - def each: () { (String) -> void } -> void - | () -> Enumerator[String, void] + def each: (?buffer_size: Integer) { (String) -> void } -> void + | (?buffer_size: Integer) -> Enumerator[String, void] def to_s: () -> untyped alias to_str to_s def stream!: () -> untyped diff --git a/test/http/response/body_test.rb b/test/http/response/body_test.rb index 804ac2b2..dd08b6ea 100644 --- a/test/http/response/body_test.rb +++ b/test/http/response/body_test.rb @@ -95,6 +95,34 @@ def test_each_yields_each_chunk assert_equal "Hello, World!", collected.join end + def test_each_passes_buffer_size_to_underlying_readpartial + received_sizes = [] + conn = Object.new + conn.define_singleton_method(:readpartial) do |size = nil| + received_sizes << size + raise EOFError if received_sizes.size > 1 + + "data" + end + body = HTTP::Response::Body.new(conn, encoding: Encoding::UTF_8) + body.each(buffer_size: 1_048_576) { |chunk| chunk } + + assert_equal [1_048_576, 1_048_576], received_sizes + end + + def test_each_defaults_buffer_size_to_connection_buffer_size + received_sizes = [] + conn = Object.new + conn.define_singleton_method(:readpartial) do |size = nil| + received_sizes << size + raise EOFError + end + body = HTTP::Response::Body.new(conn, encoding: Encoding::UTF_8) + body.each { |chunk| chunk } + + assert_equal [HTTP::Connection::BUFFER_SIZE], received_sizes + end + # --------------------------------------------------------------------------- # #to_s when streaming # ---------------------------------------------------------------------------