In Rack 3, why must middleware not call each on a response body, and how should it wrap or buffer a body safely?
answer
- enumerable body vs streaming body
- each and call run only once
- to_ary means safe to buffer
- return a new body that wraps the old
- close always, Rack::BodyProxy
basics
~20 sA Rack 3 body may be consumed only once, or may stream without each, so middleware iterating it steals the server's copy. Middleware buffers only bodies with to_ary and otherwise returns a wrapping body that closes the original.
solid answer
~40 sIn Rack 3 the body is either an **enumerable body** (responds to `each`, which may be called only once) or a **streaming body** (responds only to `call(stream)`). The SPEC forbids middleware calling `each`: that consumes a single-use body before the server sends it, and a streaming body may lack `each`. If the body responds to `to_ary`, its content is available as an Array, so middleware may call `to_ary`, use the Array, and put it back as the body, which is what `Rack::ETag` and `Rack::ContentLength` do. Otherwise it returns a new body object whose `each` calls the original's `each` and yields at least once per chunk. Whoever replaces a body must still `close` the original; `Rack::BodyProxy` delegates to the body and runs a block once on `close`.
code
ruby · 27 linesclass Upcase
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
return [status, headers, body] unless body.respond_to?(:each)
headers.delete("content-length")
[status, headers, UpcaseBody.new(body)]
end
class UpcaseBody
def initialize(body)
@body = body
end
def each
@body.each { |chunk| yield chunk.upcase }
end
def close
@body.close if @body.respond_to?(:close)
end
end
endgo deeper
Recall that a body is usually an Array of Strings but may be something that streams, and that middleware should pass it on rather than read it.
Explain enumerable versus streaming bodies, what to_ary signals, and why a replacement body must close the original.
Write body-transforming middleware that keeps streaming intact, buffers only through to_ary, and uses Rack::BodyProxy for post-response work; debug leaks from bodies nobody closed.
Judge which cross-cutting features may buffer responses at all, since each one that does turns a streamed download into memory pressure for the whole fleet.
## Two kinds of body Rack 3's SPEC splits the response body into two kinds: - An **enumerable body** responds to `each`, which must yield only Strings, may be called **only once**, and must not be called after `close`. An Array of Strings is the common case; a lazy object that yields rows as it reads them is another. - A **streaming body** responds to `call(stream)`. The server calls it once with a stream object that responds to `read`, `write`, `<<`, `flush`, `close`, `close_read`, `close_write` and `closed?`, and the body writes to the client directly. A `Proc` is the usual example. If a body responds to both, it is treated as enumerable and only `each` is called. Optional extras: `to_ary` (returns an Array identical to what `each` yields), `to_path` (a file path the server may send directly, or `nil`) and `close`. ## Why middleware must not call each The rule, in the SPEC's words: middleware "must not call `each` directly on the Body. Instead, middleware can return a new Body that calls `each` on the original Body". The reasons: 1. **Single use.** An enumerable body may be generated on the fly from a database cursor or a file. If middleware iterates it, the server's later iteration gets nothing, or raises. 2. **Memory.** Iterating to inspect usually means collecting the chunks, which turns a streamed multi-gigabyte export into one String. 3. **Streaming bodies.** A body that only responds to `call` has no `each`; code that assumes `each` crashes. `Rack::Lint` enforces this: it raises `Middleware must not call #each directly` and checks that a replacement body `yield`s at least once per chunk of the original. ## The three safe patterns | Situation | Pattern | Shipped example | |---|---|---| | Body responds to `to_ary` | call `to_ary`, work on the Array, put it back as the body | `Rack::ETag`, `Rack::ContentLength` | | Need to transform chunks | return a new body whose `each` wraps the old `each` | `Rack::Deflater` | | Need code after sending | wrap the body in `Rack::BodyProxy` | timing or cleanup middleware | **Buffering via `to_ary`.** `to_ary` is the body's statement that its content is already available as an Array, so calling it cannot consume anything that the server still needs. The SPEC adds that a body with both `to_ary` and `close` must close itself inside `to_ary`. `Rack::ETag` only computes a digest when the body responds to `to_ary`, then assigns the Array back as the response body; `Rack::ContentLength` does the same before summing `bytesize`. A streamed body therefore gets no ETag and no computed `content-length`, which is the correct outcome. **Wrapping.** A wrapper body keeps the original, implements `each` by iterating it and yielding transformed chunks, and implements `close` by closing the original. The server consumes the wrapper once, which consumes the original once. **`Rack::BodyProxy`.** `Rack::BodyProxy.new(body) { ... }` forwards every method to the body through `method_missing`, and its `close` closes the body and then runs the block, exactly once. Its `to_ary` also calls `close`, as the SPEC requires. It is the standard way to release a lock, a connection or a timer when the response is finished. ## A checklist for body-touching middleware - Check `respond_to?(:each)` before assuming an enumerable body; pass a streaming body through unless you can wrap `call`. - Check `respond_to?(:to_ary)` before buffering, and assign the returned Array back into the response. - Drop or recompute `content-length` when the transformation changes the byte count. - Delegate `close` from any wrapper to the original body. - Keep `to_path` off a wrapper whose output differs from the file, or the server may send the untransformed file instead. - Run the middleware's tests through `Rack::Lint`, which checks every one of these rules. ## close is not optional The body must be either consumed or returned, and if it responds to `close`, that must be called **at least once** - normally by the server after sending, but also by any code that makes an internal request and discards the response. A middleware that swaps the body for a new one takes over that duty for the original. Forgetting it leaks file handles and database connections; `Rack::Lint` reports `Body has not been closed`. ## A streaming body in practice ```ruby body = proc do |stream| 3.times { |i| stream.write("tick #{i}\n") } ensure stream.close end [200, {"content-type" => "text/plain"}, body] ``` Because `Proc` responds to `call` but not `each`, the server treats it as a streaming body. Servers may also expose `rack.response_finished` in `env`: an Array of callables run with `env, status, headers, error` after the response ends, in reverse order - useful when logic must run after the body is done but does not need to be tied to one body object.
- Why does Rack::ETag add no etag header to some responses?It computes the digest only for status 200 or 201 when the body responds to `to_ary`, and skips responses that already carry `etag` or `last-modified`. A streaming or lazily enumerated body without `to_ary` cannot be buffered safely, so the middleware leaves it alone instead of reading it.
- Which object is responsible for closing the original body when middleware replaces it?The middleware that replaced it. The server only sees and closes the new body, so the new body's `close` must close the original. Wrapping with `Rack::BodyProxy` or delegating `close` in a wrapper class keeps that chain intact; the SPEC states the replacement must close the original if possible.
saying these in an interview costs you the question
- Calling body.each in middleware to log or measure the response size
- Assuming every Rack 3 body responds to each
- Replacing the body without closing the original one
- Joining the body with body.join without checking for to_ary
- Treating a body with both each and call as a streaming body