You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
build_multipart loops forever (unbounded memory) when an array param holds a plain value before a Hash #368
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=truerequire"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:
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:
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.
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?
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.
Activity
jeremyevans commentedon Oct 21, 2026
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?
gabriel-canaan commentedon Oct 21, 2026
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.
jeremyevans commentedon Oct 21, 2026
Sure, I can work on a 2.3.0 release tomorrow.
gabriel-canaan commentedon Oct 21, 2026
jeremyevans commentedon Oct 21, 2026
I just released rack-test 2.3.0 with a fix for this.
gabriel-canaan commentedon Oct 21, 2026