From f2dbebb58545bb38a7a096da20406066be33d10f Mon Sep 17 00:00:00 2001 From: anyingiit <49945850+anyingiit@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:13:39 +0800 Subject: [PATCH] Add Rack::TempfileReaper to the endpoint middleware stack Multipart uploads parsed by Rack::Multipart create tempfiles that were left open on disk after the request unless the host app (e.g. Rails) already ran Rack::TempfileReaper. Use it in every endpoint's stack so they are closed and unlinked once the response body is closed. Fixes #2487 --- CHANGELOG.md | 1 + lib/grape/endpoint.rb | 1 + spec/grape/integration/rack_spec.rb | 40 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034d240cd..598636581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#3008](https://github.com/ruby-grape/grape/pull/3008): Add the grape-on-rack app as a live-server memory profiling harness - [@ericproulx](https://github.com/ericproulx). * [#3025](https://github.com/ruby-grape/grape/pull/3025): Add `:md` and `:markdown` formatters that render an object's `to_markdown`, for APIs that declare a `text/markdown` content type - [@ericproulx](https://github.com/ericproulx). * [#3029](https://github.com/ruby-grape/grape/pull/3029): Add a built-in `:jsonl` (JSON Lines) format that streams an Enumerator body - [@ericproulx](https://github.com/ericproulx). +* [#3041](https://github.com/ruby-grape/grape/pull/3041): Add `Rack::TempfileReaper` to every endpoint's middleware stack so multipart upload tempfiles are unlinked after the response - [@anyingiit](https://github.com/anyingiit). * Your contribution here. #### Fixes diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index fd68663f2..96f0c2c2f 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -462,6 +462,7 @@ def build_stack content_types = setting.content_types format = setting.format + stack.use Rack::TempfileReaper stack.use Rack::Head stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types) diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index 174da5858..e39d19134 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -50,6 +50,46 @@ end end + # https://github.com/ruby-grape/grape/issues/2487 + describe 'multipart tempfiles' do + let(:uploaded_paths) { [] } + + let(:app) do + paths = uploaded_paths + Class.new(Grape::API) do + params do + requires :file, type: File + end + + post 'upload' do + paths << params[:file][:tempfile].path + 'ok' + end + + post 'failed_upload' do + paths << params[:file][:tempfile].path + error!('failed', 422) + end + end + end + + let(:upload) { Rack::Test::UploadedFile.new(__FILE__, 'text/plain') } + + it 'unlinks the tempfile once the response is closed' do + post '/upload', file: upload + expect(last_response.body).to eq('ok') + expect(uploaded_paths.size).to eq(1) + expect(File.exist?(uploaded_paths.first)).to be(false) + end + + it 'unlinks the tempfile when the endpoint returns an error' do + post '/failed_upload', file: upload + expect(last_response.status).to eq(422) + expect(uploaded_paths.size).to eq(1) + expect(File.exist?(uploaded_paths.first)).to be(false) + end + end + context 'when the app is mounted' do let(:ping_mount) do Class.new(Grape::API) do