Skip to content

Commit b9ba3d8

Browse files
committed
Merge pull request #1405 from hedgesky/errors_handling_improvements
Exceptions aren't rescued in a predictable order
2 parents dc39328 + 1180019 commit b9ba3d8

15 files changed

+343
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#### Fixes
1313

14+
* [#1405](https://github.com/ruby-grape/grape/pull/1405): Fix priority of `rescue_from` clauses applying - [@hedgesky](https://github.com/hedgesky).
1415
* [#1365](https://github.com/ruby-grape/grape/pull/1365): Fix finding exception handler in error middleware - [@ktimothy](https://github.com/ktimothy).
1516
* [#1380](https://github.com/ruby-grape/grape/pull/1380): Fix `allow_blank: false` for `Time` attributes with valid values causes `NoMethodError` - [@ipkes](https://github.com/ipkes).
1617
* [#1384](https://github.com/ruby-grape/grape/pull/1384): Fix parameter validation with an empty optional nested `Array` - [@ipkes](https://github.com/ipkes).

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,30 @@ class Twitter::API < Grape::API
19691969
end
19701970
```
19711971

1972+
#### Rescuing exceptions inside namespaces
1973+
1974+
You could put `rescue_from` clauses inside a namespace and they will take precedence over ones
1975+
defined in the root scope:
1976+
1977+
```ruby
1978+
class Twitter::API < Grape::API
1979+
rescue_from ArgumentError do |e|
1980+
error!("outer")
1981+
end
1982+
1983+
namespace :statuses do
1984+
rescue_from ArgumentError do |e|
1985+
error!("inner")
1986+
end
1987+
get do
1988+
raise ArgumentError.new
1989+
end
1990+
end
1991+
end
1992+
```
1993+
1994+
Here `'inner'` will be result of handling occured `ArgumentError`.
1995+
19721996
#### Unrescuable Exceptions
19731997

19741998
`Grape::Exceptions::InvalidVersionHeader`, which is raised when the version in the request header doesn't match the currently evaluated version for the endpoint, will _never_ be rescued from a `rescue_from` block (even a `rescue_from :all`) This is because Grape relies on Rack to catch that error and try the next versioned-route for cases where there exist identical Grape endpoints with different versions.

UPGRADING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ Upgrading Grape
33

44
### Upgrading to >= 0.16.0
55

6+
#### Changed priority of `rescue_from` clauses applying
7+
8+
Since 0.16.3 `rescue_from` clauses declared inside namespace would take a priority over ones declared in the root scope.
9+
This could possibly affect those users who use different `rescue_from` clauses in root scope and in namespaces.
10+
611
#### Replace rack-mount with new router
712

813
The `Route#route_xyz` methods have been deprecated since 0.15.1.

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ module Util
106106
extend ActiveSupport::Autoload
107107
autoload :InheritableValues
108108
autoload :StackableValues
109+
autoload :ReverseStackableValues
109110
autoload :InheritableSetting
110111
autoload :StrictHashConfiguration
111112
autoload :Registrable

lib/grape/dsl/request_response.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def rescue_from(*args, &block)
122122
:base_only_rescue_handlers
123123
end
124124

125-
namespace_stackable handler_type, Hash[args.map { |arg| [arg, handler] }]
125+
namespace_reverse_stackable handler_type, Hash[args.map { |arg| [arg, handler] }]
126126
end
127127

128128
namespace_stackable(:rescue_options, options)

lib/grape/dsl/settings.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,28 @@ def namespace_stackable(key, value = nil)
9494
get_or_set :namespace_stackable, key, value
9595
end
9696

97+
def namespace_reverse_stackable(key, value = nil)
98+
get_or_set :namespace_reverse_stackable, key, value
99+
end
100+
97101
def namespace_stackable_with_hash(key)
98102
settings = get_or_set :namespace_stackable, key, nil
99103
return if settings.blank?
100104
settings.each_with_object({}) { |value, result| result.deep_merge!(value) }
101105
end
102106

107+
def namespace_reverse_stackable_with_hash(key)
108+
settings = get_or_set :namespace_reverse_stackable, key, nil
109+
return if settings.blank?
110+
result = {}
111+
settings.each do |setting|
112+
setting.each do |field, value|
113+
result[field] ||= value
114+
end
115+
end
116+
result
117+
end
118+
103119
# (see #unset_global_setting)
104120
def unset_namespace_stackable(key)
105121
unset :namespace_stackable, key

lib/grape/endpoint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def build_stack
256256
default_error_formatter: namespace_inheritable(:default_error_formatter),
257257
error_formatters: namespace_stackable_with_hash(:error_formatters),
258258
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
259-
rescue_handlers: namespace_stackable_with_hash(:rescue_handlers) || {},
259+
rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
260260
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
261261
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
262262

lib/grape/middleware/error.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def find_handler(klass)
5656

5757
def rescuable?(klass)
5858
return false if klass == Grape::Exceptions::InvalidVersionHeader
59-
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, _handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
59+
rescue_all? || rescue_class_or_its_ancestor?(klass) || rescue_with_base_only_handler?(klass)
6060
end
6161

6262
def exec_handler(e, &handler)
@@ -109,6 +109,20 @@ def format_message(message, backtrace)
109109
throw :error, status: 406, message: "The requested format '#{format}' is not supported." unless formatter
110110
formatter.call(message, backtrace, options, env)
111111
end
112+
113+
private
114+
115+
def rescue_all?
116+
options[:rescue_all]
117+
end
118+
119+
def rescue_class_or_its_ancestor?(klass)
120+
(options[:rescue_handlers] || []).any? { |error, _handler| klass <= error }
121+
end
122+
123+
def rescue_with_base_only_handler?(klass)
124+
(options[:base_only_rescue_handlers] || []).include?(klass)
125+
end
112126
end
113127
end
114128
end

lib/grape/util/inheritable_setting.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module Util
33
# A branchable, inheritable settings object which can store both stackable
44
# and inheritable values (see InheritableValues and StackableValues).
55
class InheritableSetting
6-
attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable
6+
attr_accessor :route, :api_class, :namespace
7+
attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable
78
attr_accessor :parent, :point_in_time_copies
89

910
# Retrieve global settings.
@@ -28,6 +29,7 @@ def initialize
2829
# used with a mount, or should every API::Class be a separate namespace by default?
2930
self.namespace_inheritable = InheritableValues.new
3031
self.namespace_stackable = StackableValues.new
32+
self.namespace_reverse_stackable = ReverseStackableValues.new
3133

3234
self.point_in_time_copies = []
3335

@@ -50,6 +52,7 @@ def inherit_from(parent)
5052

5153
namespace_inheritable.inherited_values = parent.namespace_inheritable
5254
namespace_stackable.inherited_values = parent.namespace_stackable
55+
namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable
5356
self.route = parent.route.merge(route)
5457

5558
point_in_time_copies.map { |cloned_one| cloned_one.inherit_from parent }
@@ -67,6 +70,7 @@ def point_in_time_copy
6770
new_setting.namespace = namespace.clone
6871
new_setting.namespace_inheritable = namespace_inheritable.clone
6972
new_setting.namespace_stackable = namespace_stackable.clone
73+
new_setting.namespace_reverse_stackable = namespace_reverse_stackable.clone
7074
new_setting.route = route.clone
7175
new_setting.api_class = api_class
7276

@@ -87,7 +91,8 @@ def to_hash
8791
route: route.clone,
8892
namespace: namespace.to_hash,
8993
namespace_inheritable: namespace_inheritable.to_hash,
90-
namespace_stackable: namespace_stackable.to_hash
94+
namespace_stackable: namespace_stackable.to_hash,
95+
namespace_reverse_stackable: namespace_reverse_stackable.to_hash
9196
}
9297
end
9398
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module Grape
2+
module Util
3+
class ReverseStackableValues
4+
attr_accessor :inherited_values
5+
attr_accessor :new_values
6+
7+
def initialize(inherited_values = {})
8+
@inherited_values = inherited_values
9+
@new_values = {}
10+
end
11+
12+
def [](name)
13+
[].tap do |value|
14+
value.concat(@new_values[name] || [])
15+
value.concat(@inherited_values[name] || [])
16+
end
17+
end
18+
19+
def []=(name, value)
20+
@new_values[name] ||= []
21+
@new_values[name].push value
22+
end
23+
24+
def delete(key)
25+
new_values.delete key
26+
end
27+
28+
def keys
29+
(@new_values.keys + @inherited_values.keys).sort.uniq
30+
end
31+
32+
def to_hash
33+
keys.each_with_object({}) do |key, result|
34+
result[key] = self[key]
35+
end
36+
end
37+
38+
def initialize_copy(other)
39+
super
40+
self.inherited_values = other.inherited_values
41+
self.new_values = other.new_values.dup
42+
end
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)