Skip to content

Fix rescue all header versioning regression #1114

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

Merged
Merged
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 @@ -7,6 +7,7 @@

#### Fixes

* [#1114](https://github.com/ruby-grape/grape/pull/1114): Fix regression which broke identical endpoints with different versions - [@suan](https://github.com/suan).
* [#1109](https://github.com/ruby-grape/grape/pull/1109): Memoize Virtus attribute and fix memory leak - [@marshall-lee](https://github.com/marshall-lee).
* [#1101](https://github.com/intridea/grape/pull/1101): Fix: Incorrect media-type `Accept` header now correctly returns 406 with `strict: true` - [@elliotlarson](https://github.com/elliotlarson).
* [#1108](https://github.com/ruby-grape/grape/pull/1039): Raise a warning when `desc` is called with options hash and block - [@rngtng](https://github.com/rngtng).
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,10 @@ rescue_from RuntimeError, rescue_subclasses: false do |e|
end
```

#### Unrescuable Exceptions

`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.

### Rails 3.x

When mounted inside containers, such as Rails 3.x, errors like "404 Not Found" or
Expand Down
8 changes: 8 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Upgrading Grape
===============

### Upgrading to >= 0.13.1

#### Changes to header versioning and invalid header version handling

Identical endpoints with different versions now work correctly. A regression introduced in Grape 0.11.0 caused all but the first-mounted version for such an endpoint to wrongly throw an `InvalidAcceptHeader`. As a side effect, requests with a correct vendor but invalid version can no longer be rescued from a `rescue_from` block.

See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.

### Upgrading to >= 0.12.0

#### Changes in middleware
Expand Down
1 change: 1 addition & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module Exceptions
autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type'
autoload :InvalidMessageBody
autoload :InvalidAcceptHeader
autoload :InvalidVersionHeader
end

module ErrorFormatter
Expand Down
10 changes: 10 additions & 0 deletions lib/grape/exceptions/invalid_version_header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# encoding: utf-8
module Grape
module Exceptions
class InvalidVersionHeader < Base
def initialize(message, headers)
super(message: compose_message('invalid_version_header', message: message), status: 406, headers: headers)
end
end
end
end
3 changes: 3 additions & 0 deletions lib/grape/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ en:
invalid_accept_header:
problem: 'Invalid accept header'
resolution: '%{message}'
invalid_version_header:
problem: 'Invalid version header'
resolution: '%{message}'

1 change: 1 addition & 0 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def find_handler(klass)
end

def rescuable?(klass)
return false if klass == Grape::Exceptions::InvalidVersionHeader
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, _handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
end

Expand Down
30 changes: 24 additions & 6 deletions lib/grape/middleware/versioner/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,26 @@ def before

if media_type
media_type_header_handler
elsif headers_contain_wrong_vendor_or_version?
fail_with_invalid_accept_header!('API vendor or version not found.')
elsif headers_contain_wrong_vendor?
fail_with_invalid_accept_header!('API vendor not found.')
elsif headers_contain_wrong_version?
fail_with_invalid_version_header!('API version not found.')
end
end

private

def strict_header_checks
strict_accept_header_presence_check
strict_verion_vendor_accept_header_presence_check
strict_version_vendor_accept_header_presence_check
end

def strict_accept_header_presence_check
return unless header.qvalues.empty?
fail_with_invalid_accept_header!('Accept header must be set.')
end

def strict_verion_vendor_accept_header_presence_check
def strict_version_vendor_accept_header_presence_check
return unless versions.present?
return if an_accept_header_with_version_and_vendor_is_present?
fail_with_invalid_accept_header!('API vendor or version not found.')
Expand Down Expand Up @@ -85,6 +87,11 @@ def fail_with_invalid_accept_header!(message)
.new(message, error_headers)
end

def fail_with_invalid_version_header!(message)
fail Grape::Exceptions::InvalidVersionHeader
.new(message, error_headers)
end

def available_media_types
available_media_types = []

Expand All @@ -107,9 +114,15 @@ def available_media_types
available_media_types.flatten
end

def headers_contain_wrong_vendor_or_version?
def headers_contain_wrong_vendor?
header.values.all? do |header_value|
vendor?(header_value) && request_vendor(header_value) != vendor
end
end

def headers_contain_wrong_version?
header.values.all? do |header_value|
vendor?(header_value) || version?(header_value)
version?(header_value)
end
end

Expand Down Expand Up @@ -159,6 +172,11 @@ def vendor?(media_type)
subtype[/\Avnd\.[a-z0-9*.]+/]
end

def request_vendor(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
end

# @param [String] media_type a content type
# @return [Boolean] whether the content type sets an API version
def version?(media_type)
Expand Down
14 changes: 0 additions & 14 deletions spec/grape/exceptions/invalid_accept_header_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@ def app
end

context 'that receives' do
context 'an invalid version in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a rescued request'
end
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
Expand Down Expand Up @@ -128,13 +121,6 @@ def app
end

context 'that receives' do
context 'an invalid version in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a rescued request'
end
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
Expand Down
62 changes: 56 additions & 6 deletions spec/grape/middleware/versioner/header_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include 'API vendor or version not found'
expect(exception.message).to include 'API vendor not found'
end
end

Expand All @@ -116,7 +116,7 @@
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor or version not found')
expect(exception.message).to include('API vendor not found')
end
end
end
Expand All @@ -141,10 +141,10 @@

it 'fails with 406 Not Acceptable if version is invalid' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor or version not found')
expect(exception.message).to include('API version not found')
end
end
end
Expand Down Expand Up @@ -244,10 +244,60 @@

it 'fails with another version' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor or version not found')
expect(exception.message).to include('API version not found')
end
end
end

context 'when there are multiple versions specified with rescue_from :all' do
subject {
Class.new(Grape::API) do
rescue_from :all
end
}

let(:v1_app) {
Class.new(Grape::API) do
version 'v1', using: :header, vendor: 'test'
resources :users do
get :hello do
'one'
end
end
end
}

let(:v2_app) {
Class.new(Grape::API) do
version 'v2', using: :header, vendor: 'test'
resources :users do
get :hello do
'two'
end
end
end
}

def app
subject.mount v1_app
subject.mount v2_app
subject
end

context 'with header versioned endpoints and a rescue_all block defined' do
it 'responds correctly to a v1 request' do
versioned_get '/users/hello', 'v1', using: :header, vendor: 'test'
expect(last_response.body).to eq('one')
expect(last_response.body).not_to include('API vendor or version not found')
end

it 'responds correctly to a v2 request' do
versioned_get '/users/hello', 'v2', using: :header, vendor: 'test'
expect(last_response.body).to eq('two')
expect(last_response.body).not_to include('API vendor or version not found')
end
end
end
Expand Down