Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rack >= 2.2.2 tests skip Rack::Events#on_finish #335

Closed
arielvalentin opened this issue Mar 19, 2023 · 3 comments
Closed

Rack >= 2.2.2 tests skip Rack::Events#on_finish #335

arielvalentin opened this issue Mar 19, 2023 · 3 comments

Comments

@arielvalentin
Copy link

arielvalentin commented Mar 19, 2023

While working on migrating the OTel Instrumentation from using Rack middleware to the Events API we ran into some test failures starting with Rack >= 2.2.2.

It seems that the block passed into the EventedBodyProxy that contains a call to on_finish is no longer being executed during test runs.

https://github.com/open-telemetry/opentelemetry-ruby-contrib/actions/runs/4458505381/jobs/7834300742

There do not appear to be any changes between Rack 2.2.2 and 2.2.1 that would impact this behavior, but I do not think I see any buffered response tests in rack-test so its unclear if there is some incompatibility I may have glossed over.

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'minitest'
  gem 'rack', '2.2.2'
  gem 'rack-test', '2.1.0'
end

require 'minitest/autorun'
require 'rack/test'

class MyEventHandler
  include ::Rack::Events::Abstract

  MyError = Class.new(StandardError)

  def on_start(_, _)
    $stderr.puts __method__
  end

  def on_send(_, _)
    $stderr.puts __method__
  end

  def on_commit(_, _)
    $stderr.puts __method__
  end

  def on_finish(_, _)
    raise MyError, 'unexpected'
  end

  def on_error(_, _, _)
    $stderr.puts __method__
  end
end

class BufferedTest < MiniTest::Test
  include Rack::Test::Methods

  def app
    Rack::Builder.new.tap do |builder|
      builder.use Rack::Events, [MyEventHandler.new]
      builder.run ->(_env) { [200, { 'content-type' => 'text/plain' }, Rack::BodyProxy.new('All responses are OK') { puts 'Hello world' }] }
    end
  end

  def test_root
    assert_raises MyEventHandler::MyError do
      get '/'
    end
  end
end
> rack-test-failure(:|✔) %
🤘 ruby test.rb
Run options: --seed 35637

# Running:

on_start
on_commit
F

Finished in 0.004877s, 205.0441 runs/s, 205.0441 assertions/s.

  1) Failure:
BufferedTest#test_root [test.rb:50]:
MyEventHandler::MyError expected but nothing was raised.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

Any suggestions or thoughts as to why we may be running into this problem?

@arielvalentin
Copy link
Author

arielvalentin commented Mar 19, 2023

For comparison with Rack 2.2.1 and Rack-Test 2.1.0

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'minitest'
  gem 'rack', '2.2.1'
  gem 'rack-test', '2.1.0'
end

require 'minitest/autorun'
require 'rack/test'

class MyEventHandler
  include ::Rack::Events::Abstract

  MyError = Class.new(StandardError)

  def on_start(_, _)
    $stderr.puts __method__
  end

  def on_send(_, _)
    $stderr.puts __method__
  end

  def on_commit(_, _)
    $stderr.puts __method__
  end

  def on_finish(_, _)
    raise MyError, 'unexpected'
  end

  def on_error(_, _, _)
    $stderr.puts __method__
  end
end

class BufferedTest < MiniTest::Test
  include Rack::Test::Methods

  def app
    Rack::Builder.new.tap do |builder|
      builder.use Rack::Events, [MyEventHandler.new]
      builder.run ->(_env) { [200, { 'content-type' => 'text/plain' }, Rack::BodyProxy.new('All responses are OK') { puts 'Hello world' }] }
    end
  end

  def test_root
    assert_raises MyEventHandler::MyError do
      get '/'
    end
  end
end
vibora ~/github/rack-test-failure(:|✔) %
🤘 ruby test.rb
Run options: --seed 43962

# Running:

on_start
on_commit
Hello world
.

Finished in 0.005103s, 195.9632 runs/s, 195.9632 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

@jeremyevans
Copy link
Contributor

It helps if you pass Rack::BodyProxy a valid rack body :). Change to Rack::BodyProxy.new(['All responses are OK']) and the test works.

As to why the behavior is skipped for the invalid body, because it is a plain string (in a wrapper that still responds to to_str), Rack::Response treats it as a plain string, and implicitly wraps it in its own array, so the on_finish parts are never called. Potentially, you can consider that a bug in rack, but as you can only trigger it by providing an invalid rack response, it is debatable whether it is a bug or not. However, it's definitely not a bug in rack-test, so I'm closing this.

@arielvalentin
Copy link
Author

Thanks @jeremyevans!!! I should have been more diligent in verifying my code was correct so I appreciate you taking the time to review this.

🙇🏼

jeremyevans added a commit to jeremyevans/rack that referenced this issue Mar 19, 2023
This methods could trigger different behavior in rack that is
undesired when using BodyProxy.  When using BodyProxy, you always
want the caller to iterate through the body using each.

See rack/rack-test#335 for an example
where allowing BodyProxy to respond to to_str (when provided an
invalid rack body) complicated debugging.

BodyProxy already had a spec with a description
"not respond to :to_ary".  While you would assume that this checked
whether the body actually responded to to_ary, it did not.  This
fixes that, making sure that respond_to?(:to_ary) is false, and
calling to_ary raises a NoMethodError.  It adds a similar spec for
to_str.

If body passed to BodyProxy responds to to_path, have the to_path
method close the body proxy.  Without this, use of Rack::Lock
with a body that responds to to_path would result in the mutex
remaining locked if the server calls to_path and does not iterate
over the body (which is allowed by SPEC).
jeremyevans added a commit to jeremyevans/rack that referenced this issue Mar 19, 2023
This methods could trigger different behavior in rack that is
undesired when using BodyProxy.  When using BodyProxy, you always
want the caller to iterate through the body using each.

See rack/rack-test#335 for an example
where allowing BodyProxy to respond to to_str (when provided an
invalid rack body) complicated debugging.

BodyProxy already had a spec with a description
"not respond to :to_ary".  While you would assume that this checked
whether the body actually responded to to_ary, it did not.  This
fixes that, making sure that respond_to?(:to_ary) is false, and
calling to_ary raises a NoMethodError.  It adds a similar spec for
to_str.
jeremyevans added a commit to rack/rack that referenced this issue Mar 21, 2023
See rack/rack-test#335 for an example
where allowing BodyProxy to respond to to_str (when provided an
invalid rack body) complicated debugging.

Call BodyProxy#close if BodyProxy#to_ary is called, as not doing so
violates SPEC.
Earlopain pushed a commit to Earlopain/rack that referenced this issue May 9, 2024
See rack/rack-test#335 for an example
where allowing BodyProxy to respond to to_str (when provided an
invalid rack body) complicated debugging.

Call BodyProxy#close if BodyProxy#to_ary is called, as not doing so
violates SPEC.
ioquatix pushed a commit to rack/rack that referenced this issue May 9, 2024
* Do not allow BodyProxy to respond to to_str, make to_ary call close

See rack/rack-test#335 for an example
where allowing BodyProxy to respond to to_str (when provided an
invalid rack body) complicated debugging.

Call BodyProxy#close if BodyProxy#to_ary is called, as not doing so
violates SPEC.

* Changelog for #2062

---------

Co-authored-by: Jeremy Evans <code@jeremyevans.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants