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 each and call must be treated as an Enumerable Body, not a Streaming Body. If it responds to each, you must call each and not call. If the Body doesn't respond to each, then you can assume it responds to call.
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
end
This 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.
When
Rack::Eventsis in the middleware chain, it causes responses with streaming bodies (where the body responds to#calland not#each) to fail with aNoMethodError.Root cause analysis
The
Rack::Eventsmiddleware allows one or more handlers to react to events related to a request. One of these events ison_send, which is triggered when the response is sent.In order to implement
on_send,Rack::Eventswraps the body that is passed down the middleware chain in aRack::Events::EventedBodyProxy, a subclass ofBodyProxywith a custom#eachimplementation that calls theon_sendmethod on the handlers and calls#eachon the proxied body.According to the spec, given a body that implements both
#eachand#call, a conforming Rack server must call#each. Streaming bodies must therefore only implement#call:Due to
EventedBodyProxy's custom#eachinstrumentation, 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#eachon theEventedBodyProxy, which will in turn call#eachon the body it proxies -- even when the proxied body does not implement#each.Quick "fix"
In
EventedBodyProxy's implementation:This should cause it to only respond to
#eachif 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_sendevent for streaming bodies.EventedBodyProxydoes not implement#call, and so it will proxy it directly to the proxied body (as perBodyProxy'smethod_missing, which it inherits from) without ever callingon_sendon the handlers. A proper fix, I believe, would need to callon_sendwhencloseis called on the stream, probably by wrapping it in some sort ofEventedStreamProxy.Reproduction case
Run
bundle install, thenbundle exec rackup. Uncomment theuse Rack::Eventsline 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.