Skip to content

Commit

Permalink
Allow rack/mount cascading to be skipped on header
Browse files Browse the repository at this point in the history
When using header versioning with the :strict policy, Grape currently
returns the appropriate error (406 Not Acceptable) but it also returns a
X-Cascade header indicating to rack it should keep looking for
other routes.

While this behavior is expected for various applications, some may need
to skip the cascading and explicitly return the error to the requester.

For that, a :cascade option was added to the version method. The default
value is true (keeping backwards compat), meaning that 406 errors shall
be raised along with X-Cascade headers, just like before. Though, if set
to false, it 406 errors shall be raised with no X-Cascade headers,
therefore telling the router it should not lookup other routes.
  • Loading branch information
dieb committed Feb 18, 2013
1 parent 3757db2 commit c81af68
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Next Release
============

* [#340](https://github.com/intridea/grape/pull/339): Allow rack/mount cascading to be skipped on header - [@dieb](https://github.com/dieb).
* [#333](https://github.com/intridea/grape/pull/333): Validation for array in params - [@flyerhzm](https://github.com/flyerhzm).
* [#306](https://github.com/intridea/grape/issues/306): Added I18n support for all Grape exceptions - [@niedhui](https://github.com/niedhui).
* [#294](https://github.com/intridea/grape/issues/294): Extracted `Grape::Entity` into a [grape-entity](https://github.com/agileanimal/grape-entity) gem - [@agileanimal](https://github.com/agileanimal).
Expand Down
10 changes: 7 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ Using this versioning strategy, clients should pass the desired version in the H
By default, the first matching version is used when no `Accept` header is
supplied. This behavior is similar to routing in Rails. To circumvent this default behavior,
one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error
is returned when no correct `Accept` header is supplied.
is returned when no correct `Accept` header is supplied. By default this error contains a
`X-Cascade` header set to `pass`, allowing nesting and stacking of routes (See
[Rack::Mount](https://github.com/josh/rack-mount) for more information). To circumvent this default
behavior, one can set the `:cascade` option to `false`, indicating the `X-Cascade` header may not
be passed.

### Path

Expand Down Expand Up @@ -500,7 +504,7 @@ redirect "/statuses", :permanent => true
## Allowed Methods

When you add a `GET` route for a resource, a route for the `HEAD`
method will also be added automatically. You can disable this
method will also be added automatically. You can disable this
behavior with `do_not_route_head!`.

``` ruby
Expand Down Expand Up @@ -907,7 +911,7 @@ formatter.

## Authentication

### Basic and Digest Auth
### Basic and Digest Auth

Grape has built-in Basic and Digest authentication.

Expand Down
16 changes: 12 additions & 4 deletions lib/grape/middleware/versioner/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ def before
if strict?
# If no Accept header:
if header.qvalues.empty?
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must be set.'
throw :error, :status => 406, :headers => error_headers, :message => 'Accept header must be set.'
end
# Remove any acceptable content types with ranges.
header.qvalues.reject! do |media_type,_|
Rack::Accept::Header.parse_media_type(media_type).find{|s| s == '*'}
end
# If all Accept headers included a range:
if header.qvalues.empty?
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must not contain ranges ("*").'
throw :error, :status => 406, :headers => error_headers, :message => 'Accept header must not contain ranges ("*").'
end
end

Expand All @@ -55,10 +55,10 @@ def before
end
# If none of the available content types are acceptable:
elsif strict?
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => '406 Not Acceptable'
throw :error, :status => 406, :headers => error_headers, :message => '406 Not Acceptable'
# If all acceptable content types specify a vendor or version that doesn't exist:
elsif header.values.all?{ |media_type| has_vendor?(media_type) || has_version?(media_type)}
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'API vendor or version not found.'
throw :error, :status => 406, :headers => error_headers, :message => 'API vendor or version not found.'
end
end

Expand Down Expand Up @@ -95,6 +95,14 @@ def strict?
options[:version_options] && options[:version_options][:strict]
end

def cascade?
options[:version_options] && (options[:version_options][:cascade].nil? ? true : options[:version_options][:cascade])
end

def error_headers
cascade? ? {'X-Cascade' => 'pass'} : {}
end

# @param [String] media_type a content type
# @return [Boolean] whether the content type sets a vendor
def has_vendor?(media_type)
Expand Down
56 changes: 56 additions & 0 deletions spec/grape/middleware/versioner/header_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,60 @@
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first.should == 200
end
end

context 'when :strict and :cascade=>false are set' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
@options[:version_options][:cascade] = false
end

it 'fails with 406 Not Acceptable if header is not set' do
expect {
env = subject.call({}).last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept header must be set.'
)
end

it 'fails with 406 Not Acceptable if header is empty' do
expect {
env = subject.call('HTTP_ACCEPT' => '').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept header must be set.'
)
end

it 'fails with 406 Not Acceptable if type is a range' do
expect {
env = subject.call('HTTP_ACCEPT' => '*/*').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept header must not contain ranges ("*").'
)
end

it 'fails with 406 Not Acceptable if subtype is a range' do
expect {
env = subject.call('HTTP_ACCEPT' => 'application/*').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept header must not contain ranges ("*").'
)
end

it 'succeeds if proper header is set' do
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first.should == 200
end
end
end

0 comments on commit c81af68

Please sign in to comment.