Skip to content

Modify the priority of :any routes, fixes #1089 #1517

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

Closed
wants to merge 3 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
#### Fixes

* [#1505](https://github.com/ruby-grape/grape/pull/1505): Run `before` and `after` callbacks, but skip the rest when handling OPTIONS - [@jlfaber](https://github.com/jlfaber).
* [#1517](https://github.com/ruby-grape/grape/pull/1517), [#1089](https://github.com/ruby-grape/grape/pull/1089): Fix: priority of ANY routes - [@namusyaka](https://github.com/namusyaka), [@wagenet](https://github.com/wagenet).

0.18.0 (10/7/2016)
==================
Expand Down
16 changes: 16 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ Upgrading Grape

### Upgrading to >= 0.18.1

#### Modify the priority of :any routes

Conventional, the :any routes had been searched after matching first route and 405 routes.
This version adds the :any routes processing before 405 processing, so the behavior of following code will be changed.

```ruby
post :example do
'example'
end
route :any, '*path' do
error! :not_found, 404
end

get '/example' #=> before: 405, currently: 404
```

#### Removed param processing from built-in OPTIONS handler

When a request is made to the built-in `OPTIONS` handler, only the `before` and `after`
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def add_head_not_allowed_methods_and_options_methods
route_settings[:endpoint] = route.app

# using the :any shorthand produces [nil] for route methods, substitute all manually
route_settings[:methods] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if route_settings[:methods].include?('ANY')
route_settings[:methods] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if route_settings[:methods].include?('*')
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def mount(mounts)
method: :any,
path: path,
app: app,
route_options: { anchor: false },
forward_match: !app.respond_to?(:inheritable_setting),
for: self
)
Expand All @@ -118,6 +119,7 @@ def mount(mounts)
# end
# end
def route(methods, paths = ['/'], route_options = {}, &block)
methods = '*' if methods == :any
endpoint_options = {
method: methods,
path: paths,
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/http/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ module Headers
HEAD = 'HEAD'.freeze
OPTIONS = 'OPTIONS'.freeze

SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze

HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'.freeze
X_CASCADE = 'X-Cascade'.freeze
HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
Expand Down
56 changes: 36 additions & 20 deletions lib/grape/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def self.normalize_path(path)
path
end

def self.supported_methods
@supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
end

def initialize
@neutral_map = []
@map = Hash.new { |hash, key| hash[key] = [] }
Expand All @@ -28,7 +32,8 @@ def initialize
def compile!
return if compiled
@union = Regexp.union(@neutral_map.map(&:regexp))
map.each do |method, routes|
self.class.supported_methods.each do |method|
routes = map[method]
@optimized_map[method] = routes.map.with_index do |route, index|
route.index = index
route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
Expand Down Expand Up @@ -64,50 +69,65 @@ def recognize_path(input)

def identity(env)
route = nil
response = transaction(env) do |input, method, routing_args|
response = transaction(env) do |input, method|
route = match?(input, method)
if route
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
route.exec(env)
end
process_route(route, env) if route
end
[response, route]
end

def rotation(env, exact_route = nil)
response = nil
input, method, routing_args = *extract_required_args(env)
routes_for(method).each do |route|
input, method = *extract_input_and_method(env)
map[method].each do |route|
next if exact_route == route
next unless route.match?(input)
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
response = route.exec(env)
response = process_route(route, env)
break unless cascade?(response)
end
response
end

def transaction(env)
input, method, routing_args = *extract_required_args(env)
response = yield(input, method, routing_args)
input, method = *extract_input_and_method(env)
response = yield(input, method)

return response if response && !(cascade = cascade?(response))
neighbor = greedy_match?(input)
return unless neighbor

# If neighbor exists and request method is OPTIONS,
# return response by using #call_with_allow_headers.
return call_with_allow_headers(
env,
neighbor.allow_header,
neighbor.endpoint
) if neighbor && method == 'OPTIONS' && !cascade

route = match?(input, '*')
if route
response = process_route(route, env)
return response if response && !(cascade = cascade?(response))
end

(!cascade && neighbor) ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
end

def process_route(route, env)
input, = *extract_input_and_method(env)
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
route.exec(env)
end

def make_routing_args(default_args, route, input)
args = default_args || { route_info: route }
args.merge(route.params(input))
end

def extract_required_args(env)
def extract_input_and_method(env)
input = string_for(env[Grape::Http::Headers::PATH_INFO])
method = env[Grape::Http::Headers::REQUEST_METHOD]
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
[input, method, routing_args]
[input, method]
end

def with_optimization
Expand Down Expand Up @@ -141,10 +161,6 @@ def cascade?(response)
response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
end

def routes_for(method)
map[method] + map['ANY']
end

def string_for(input)
self.class.normalize_path(input)
end
Expand Down
8 changes: 7 additions & 1 deletion lib/grape/router/pattern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ def pattern_options
end

def build_path(pattern, anchor: false, suffix: nil, **_options)
pattern << '*path' unless anchor || pattern.end_with?('*path')
unless anchor || pattern.end_with?('*path')
pattern << '/' unless pattern.end_with?('/')
pattern << '*path'
end
pattern = pattern.split('/').tap do |parts|
parts[parts.length - 1] = '?' + parts.last
end.join('/') if pattern.end_with?('*path')
pattern + suffix.to_s
end

Expand Down
34 changes: 31 additions & 3 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,37 @@ class DummyFormatClass
end
end

it 'allows for catch-all in a namespace' do
subject.namespace :nested do
get do
'root'
end

get 'something' do
'something'
end

route :any, '*path' do
'catch-all'
end
end

send('get', 'nested')
Copy link
Member

@dblock dblock Nov 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticing this. This should just be get 'nested/something'..., I'll update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I missed this. Sorry ;(

expect(last_response.body).to eql 'root'

send('get', 'nested/something')
expect(last_response.body).to eql 'something'

send('get', 'nested/missing')
expect(last_response.body).to eql 'catch-all'

send('post', 'nested')
expect(last_response.body).to eql 'catch-all'

send('post', 'nested/something')
expect(last_response.body).to eql 'catch-all'
end

verbs = %w(post get head delete put options patch)
verbs.each do |verb|
it "allows and properly constrain a #{verb.upcase} method" do
Expand Down Expand Up @@ -755,9 +786,6 @@ class DummyFormatClass
subject.post :example do
'example'
end
subject.route :any, '*path' do
error! :not_found, 404
end
get '/example'
end

Expand Down