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

Rescue Child Errors In Error Middleware #544

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Next Release
* [#527](https://github.com/intridea/grape/pull/527): `before_validation` now a distinct callback (supersedes [#523](https://github.com/intridea/grape/pull/523)) - [@myitcv](https://github.com/myitcv).
* [#531](https://github.com/intridea/grape/pull/531): Helpers are now available to auth middleware, executing in the context of the endpoint - [@joelvh](https://github.com/joelvh).
* [#540](https://github.com/intridea/grape/pull/540): Ruby 2.1.0 is now supported - [@salimane](https://github.com/salimane).
* [#544](https://github.com/intridea/grape/pull/544): `rescue_from` now handles subclasses of exceptions by default - [@xevix](https://github.com/xevix).
* Your contribution here.

#### Fixes
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,41 @@ class Twitter::API < Grape::API
end
```

By default, `rescue_from` will rescue the exceptions listed and all their subclasses.

Assume you have the following exception classes defined.

```ruby
module APIErrors
class ParentError < StandardError; end
class ChildError < ParentError; end
end
```

Then the following `rescue_from` clause will rescue exceptions of type `APIErrors::ParentError` and its subclasses (in this case `APIErrors::ChildError`).

```ruby
rescue_from APIErrors::ParentError do |e|
Rack::Response.new({
error: "#{e.class} error",
message: e.message
}.to_json, e.status)
end
```

To only rescue the base exception class, set `rescue_subclasses: false`.
The code below will rescue exceptions of type `RuntimeError` but _not_ its subclasses.

```ruby
rescue_from RuntimeError, rescue_subclasses: false do |e|
Rack::Response.new(
status: e.status,
message: e.message,
errors: e.errors
}.to_json, e.status)
end
```

#### Rails 3.x

When mounted inside containers, such as Rails 3.x, errors like "404 Not Found" or
Expand Down
14 changes: 4 additions & 10 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def default_error_status(new_status = nil)
# @param [Block] block Execution block to handle the given exception.
# @param [Hash] options Options for the rescue usage.
# @option options [Boolean] :backtrace Include a backtrace in the rescue response.
# @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
# @param [Proc] handler Execution proc to handle the given exception as an
# alternative to passing a block
def rescue_from(*args, &block)
Expand All @@ -211,19 +212,12 @@ def rescue_from(*args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
handler ||= proc { options[:with] } if options.has_key?(:with)

if handler
args.each do |arg|
imbue :rescue_handlers, arg => handler
end
end
handler_type = !!options[:rescue_subclasses] ? :rescue_handlers : :base_only_rescue_handlers
imbue handler_type, Hash[args.map { |arg| [arg, handler] }]

imbue(:rescue_options, options)

if args.include?(:all)
set(:rescue_all, true)
else
imbue(:rescued_errors, args)
end
set(:rescue_all, true) if args.include?(:all)
end

# Allows you to specify a default representation entity for a
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -411,11 +411,11 @@ def build_middleware
format: settings[:format],
default_status: settings[:default_error_status] || 500,
rescue_all: settings[:rescue_all],
rescued_errors: aggregate_setting(:rescued_errors),
default_error_formatter: settings[:default_error_formatter],
error_formatters: settings[:error_formatters],
rescue_options: settings[:rescue_options],
rescue_handlers: merged_setting(:rescue_handlers)
rescue_handlers: merged_setting(:rescue_handlers),
base_only_rescue_handlers: merged_setting(:base_only_rescue_handlers)

aggregate_setting(:middleware).each do |m|
m = m.dup
Expand Down
13 changes: 10 additions & 3 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ def default_options
formatters: {},
error_formatters: {},
rescue_all: false, # true to rescue all exceptions
rescue_subclasses: true, # rescue subclasses of exceptions listed
rescue_options: { backtrace: false }, # true to display backtrace
rescue_handlers: {}, # rescue handler blocks
rescued_errors: []
base_only_rescue_handlers: {} # rescue handler blocks rescuing only the base class
}
end

Expand All @@ -30,15 +31,21 @@ def call!(env)
handler = lambda { |arg| error_response(arg) }
else
raise unless is_rescuable
handler = options[:rescue_handlers][e.class] || options[:rescue_handlers][:all]
handler = find_handler(e.class)
end

handler.nil? ? handle_error(e) : exec_handler(e, &handler)
end
end

def find_handler(klass)
handler = options[:rescue_handlers].find(-> { [] }) { |error, _| klass <= error }[1]
handler = options[:base_only_rescue_handlers][klass] || options[:base_only_rescue_handlers][:all] unless handler
handler
end

def rescuable?(klass)
options[:rescue_all] || (options[:rescued_errors] || []).include?(klass)
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
end

def exec_handler(e, &handler)
Expand Down
64 changes: 63 additions & 1 deletion spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ class DatabaseError < RuntimeError; end
last_response.body.should == 'rescued from DatabaseError'
end
it 'does not rescue a different error' do
class CommunicationError < RuntimeError; end
class CommunicationError < StandardError; end
subject.rescue_from RuntimeError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
Expand Down Expand Up @@ -1286,6 +1286,68 @@ def rescue_arg_error
end
end

describe '.rescue_from klass, children: method' do
it 'rescues error as well as child errors with rescue_children option set' do
class CommunicationsError < RuntimeError; end
subject.rescue_from RuntimeError, rescue_subclasses: true do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/caught_child' do
raise CommunicationsError
end
subject.get '/caught_parent' do
raise RuntimeError
end
subject.get '/uncaught_parent' do
raise StandardError
end

get '/caught_child'
last_response.status.should eql 500
get '/caught_parent'
last_response.status.should eql 500
lambda { get '/uncaught_parent' }.should raise_error(StandardError)
end

it 'does not rescue child errors for other rescue_from instances' do
class CommunicationsError < RuntimeError; end
class BadCommunicationError < CommunicationError; end

class ProcessingError < RuntimeError; end
class BadProcessingError < ProcessingError; end

subject.rescue_from CommunicationError, rescue_subclasses: true do |e|
rack_response("rescued from #{e.class.name}", 500)
end

subject.rescue_from ProcessingError, rescue_subclasses: false do |e|
rack_response("rescued from #{e.class.name}", 500)
end

subject.get '/caught_child' do
raise BadCommunicationError
end
subject.get '/uncaught_child' do
raise BadProcessingError
end

get '/caught_child'
last_response.status.should eql 500
lambda { get '/uncaught_child' }.should raise_error(BadProcessingError)
end

it 'does not rescue child errors if rescue_subclasses is false' do
class CommunicationsError < RuntimeError; end
subject.rescue_from RuntimeError, rescue_subclasses: false do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/uncaught' do
raise CommunicationError
end
lambda { get '/uncaught' }.should raise_error(CommunicationError)
end
end

describe '.error_format' do
it 'rescues all errors and return :txt' do
subject.rescue_from :all
Expand Down