Skip to content

Rack::Events breaks streaming bodies #2373

@unflxw

Description

@unflxw

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions