-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Rack::Events breaks streaming bodies #2373
Description
When Rack::Events is in the middleware chain, it causes responses with streaming bodies (where the body responds to #call and not #each) to fail with a NoMethodError.
Root cause analysis
The Rack::Events middleware allows one or more handlers to react to events related to a request. One of these events is on_send, which is triggered when the response is sent.
In order to implement on_send, Rack::Events wraps the body that is passed down the middleware chain in a Rack::Events::EventedBodyProxy, a subclass of BodyProxy with a custom #each implementation that calls the on_send method on the handlers and calls #each on the proxied body.
According to the spec, given a body that implements both #each and #call, a conforming Rack server must call #each. Streaming bodies must therefore only implement #call:
A Body that responds to both
eachandcallmust be treated as an Enumerable Body, not a Streaming Body. If it responds toeach, you must calleachand notcall. If the Body doesn't respond toeach, then you can assume it responds tocall.
Due to EventedBodyProxy's custom #each instrumentation, the body will always respond to #each, and therefore will always be treated as an enumerable body, and not as a streaming body. The Rack server will call #each on the EventedBodyProxy, which will in turn call #each on the body it proxies -- even when the proxied body does not implement #each.
Quick "fix"
In EventedBodyProxy's implementation:
def respond_to?(method, include_all=false)
return @body.respond_to?(:each, include_all) if method == :each
super
endThis should cause it to only respond to #each if the body it proxies also responds to #each, preventing it from breaking streaming bodies' requests.
This would not be a proper fix, as it does not implement the on_send event for streaming bodies. EventedBodyProxy does not implement #call, and so it will proxy it directly to the proxied body (as per BodyProxy's method_missing, which it inherits from) without ever calling on_send on the handlers. A proper fix, I believe, would need to call on_send when close is called on the stream, probably by wrapping it in some sort of EventedStreamProxy.
Reproduction case
# config.ru
require "rack/events"
class StreamingBody
def call(stream)
stream.write "Hello world!"
stream.close
end
end
# Uncomment this line to trigger the error
# use Rack::Events, []
run ->(env) {
[200, {}, StreamingBody.new]
}# Gemfile
source "https://rubygems.org"
gem "rack", "3.2.0"
gem "rackup"
gem "puma"Run bundle install, then bundle exec rackup. Uncomment the use Rack::Events line to trigger the error. Reproduced locally using Rack 3.2.0, Rackup 2.2.1 and Puma 6.6.0 on Ruby 3.1.1.