Repository: rack/rack (Ruby)

Issue:
Mounted Grape App -> Rack::Lint::LintError: Response body is already closed
Hi 👋,

I'm working on a feature to add `linting` in [Grape](https://github.com/ruby-grape/grape) by adding `Rack::Lint` in the stack. It works well but I'm encountering a `Rack::Lint::LintError: Response body is already closed` when a Grape API is mounted in a Rails app.

When calling it, Rails's middlewares stack will deal with a `Rack::Lint::Wrapper` body instead of the real one. This ain't the issue but if we look at `Rack::Etag`, it might call [body.to_ary](https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/etag.rb#L32). Indeed, the `to_ary` function will be the [one](https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/lint.rb#L906) in `Rack::Lint::Wrapper` and it will `close` the body. Unfortunately, when calling `each` on it, it will raise a `Rack::Lint::LintError` even though our original linting was valid.

I've looked into `Rack::Lint` and somehow, it was [setting](https://github.com/rack/rack/blob/df6c47357f6c6bec2d585f45f417285d813d9b3a/lib/rack/lint.rb#L734) `@closed = false` when calling `each` in Rack 2, but this is not the case anymore.

I've open an [issue](https://github.com/ruby-grape/grape/issues/2559) on our side if you want to take a look at my example.

Thanks!


Discussion: Thanks for your report. Wow, this bug has existed for a long time. Introduced in https://github.com/rack/rack/pull/1887

Looks like we do all the right things except we don't return the right body. Whoops.





Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c9dc88f3..2e5e0c918 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,10 @@ All notable changes to this project will be documented in this file. For info on
 - Fix the specification for `SERVER_PORT` which was incorrectly documented as required to be an `Integer` if present - it must be a `String` containing digits only. ([#2296](https://github.com/rack/rack/pull/2296), [@ioquatix])
 - `SERVER_NAME` and `HTTP_HOST` are now more strictly validated according to the relevant specifications. ([#2298](https://github.com/rack/rack/pull/2298), [@ioquatix])
 
+## [3.1.13] - 2025-04-13
+
+- Ensure `Rack::ETag` correctly updates response body. ([#2324](https://github.com/rack/rack/pull/2324), [@ioquatix])
+
 ## [3.1.12] - 2025-03-11
 
 ### Security
@@ -166,6 +170,10 @@ Rack v3.1 is primarily a maintenance release that removes features deprecated in
 
 - In `Rack::Files`, ignore the `Range` header if served file is 0 bytes. ([#2159](https://github.com/rack/rack/pull/2159), [@zarqman])
 
+## [3.0.15] - 2025-04-13
+
+- Ensure `Rack::ETag` correctly updates response body. ([#2324](https://github.com/rack/rack/pull/2324), [@ioquatix])
+
 ## [3.0.14] - 2025-03-11
 
 ### Security
diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb
index fa78b472f..1841beb37 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -32,6 +32,9 @@ def call(env)
         body = body.to_ary
         digest = digest_body(body)
         headers[ETAG_STRING] = %(W/"#{digest}") if digest
+
+        # Body was modified, so we need to re-assign it:
+        response[2] = body
       end
 
       unless headers[CACHE_CONTROL]