Skip to content

build_multipart loops forever (unbounded memory) when an array param holds a plain value before a Hash #368

Closed

Description

@gabriel-canaan

Versions: rack-test 2.2.0 (latest), rack 3.2.6, Ruby 4.0.7. Reading the source, the same code is in 2.1.0.

What happens

When a multipart request's params contain an array with a non-Hash value before a Hash, Rack::Test::Utils.build_multipart never returns. Memory grows until the process is killed. We saw it climb past 8 GB before an out-of-memory kill. The request never reaches the app.

Arrays of only hashes, or only plain values or uploads, work. The order matters: [{...}, upload] is fine, and ["x", {...}, upload] hangs.

Reproduction

$stdout.sync = true
require "rack/test"
require "stringio"

upload = Rack::Test::UploadedFile.new(StringIO.new("hello"), "text/plain", original_filename: "a.txt")

# Works: only hashes in the array, or only plain values.
Rack::Test::Utils.build_multipart("files" => [{ "name" => "x" }, upload])
Rack::Test::Utils.build_multipart("files" => ["x", upload])
puts "ok shapes built"

# Hangs, with memory growing without bound: a plain value BEFORE a hash.
Rack::Test::Utils.build_multipart("files" => ["x", { "name" => "y" }, upload])
puts "never reached"

Run under a memory cap (for example systemd-run --user --scope -p MemoryMax=1G -- timeout 20 ruby repro.rb). It prints ok shapes built, then is killed. It is reachable through any request helper, e.g. post path, params: { files: ["x", { "name" => "y" }, upload] } in a request spec.

Cause

In normalize_multipart_params, the Array branch maps over value:

value.map do |v|
  if v.is_a?(Hash)
    ...
    (flattened_params["#{k}[]"] ||= []) << nested_params
  else
    flattened_params["#{k}[]"] = value
  end
end

On a non-Hash element, the slot is set to the original value array itself. On a later Hash element, ||= then finds that same array, and << appends to the array that map is still iterating. Each pass adds an element, so the iteration never ends.

Suggested fix

The minimum is to never append into the array being iterated. Collect into a new array instead:

value.each do |v|
  slot = (flattened_params["#{k}[]"] ||= [])
  slot << (v.is_a?(Hash) ? normalize_multipart_params(v) : v)
end

We tried that locally, and it stops the hang. Note, though, that _build_parts only expands hashes when every element is a Hash (value.all? { |v| v.is_a?(Hash) }). So with this change, the Hash in a mixed array is sent as its inspect string rather than as a files[][name] field. A complete fix would also build the parts per element. Raising ArgumentError for a mixed array would be an acceptable alternative.

Rack's server-side parser handles the equivalent multipart body (files[]=x, files[][name]=y, files[]=<file>) in milliseconds and returns ["x", {"name" => "y"}, <file>]. So the shape is valid, and only the client-side builder loops.

Activity

  1. jeremyevans commented on Oct 21, 2026

    @jeremyevans
    Contributor

    Thank you for the report. I believe this was fixed by #361, since it doesn't seem like the main branch is affected. Can you try to reproduce with the main branch?

  2. gabriel-canaan commented on Oct 21, 2026

    @gabriel-canaan
    Author

    Thanks, confirmed. On main (173541f) the repro builds the mixed shape and completes, while 2.2.0 still hangs. It looks like 71dfcb9 ("Normalize mixed multipart arrays without mutating input") fixed it. Happy to close this. Is a release with that fix planned? Until then, 2.2.0 users can hit the hang from an ordinary request spec.

  3. jeremyevans commented on Oct 21, 2026

    @jeremyevans
    Contributor

    Sure, I can work on a 2.3.0 release tomorrow.

  4. gabriel-canaan commented on Oct 21, 2026

    @gabriel-canaan
    Author
  5. jeremyevans commented on Oct 21, 2026

    @jeremyevans
    Contributor

    I just released rack-test 2.3.0 with a fix for this.

  6. gabriel-canaan commented on Oct 21, 2026

    @gabriel-canaan
    Author
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions