Skip to content

Commit 2fa517c

Browse files
suanSuan-Aik Yeo
authored and
Suan-Aik Yeo
committed
Allow invalid header version errors to bubble up
fixes #968
1 parent ee97f89 commit 2fa517c

File tree

10 files changed

+108
-26
lines changed

10 files changed

+108
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#### Fixes
99

10+
* [#1114](https://github.com/ruby-grape/grape/pull/1114): Fix regression which broke identical endpoints with different versions - [@suan](https://github.com/suan).
1011
* [#1109](https://github.com/ruby-grape/grape/pull/1109): Memoize Virtus attribute and fix memory leak - [@marshall-lee](https://github.com/marshall-lee).
1112
* [#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).
1213
* [#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).

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,10 @@ rescue_from RuntimeError, rescue_subclasses: false do |e|
16081608
end
16091609
```
16101610

1611+
#### Unrescuable Exceptions
1612+
1613+
`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.
1614+
16111615
### Rails 3.x
16121616

16131617
When mounted inside containers, such as Rails 3.x, errors like "404 Not Found" or

UPGRADING.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Upgrading Grape
22
===============
33

4+
### Upgrading to >= 0.13.1
5+
6+
#### Changes to header versioning and invalid header version handling
7+
8+
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.
9+
10+
See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.
11+
412
### Upgrading to >= 0.12.0
513

614
#### Changes in middleware

lib/grape.rb

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ module Exceptions
6868
autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type'
6969
autoload :InvalidMessageBody
7070
autoload :InvalidAcceptHeader
71+
autoload :InvalidVersionHeader
7172
end
7273

7374
module ErrorFormatter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# encoding: utf-8
2+
module Grape
3+
module Exceptions
4+
class InvalidVersionHeader < Base
5+
def initialize(message, headers)
6+
super(message: compose_message('invalid_version_header', message: message), status: 406, headers: headers)
7+
end
8+
end
9+
end
10+
end

lib/grape/locale/en.yml

+3
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ en:
4545
invalid_accept_header:
4646
problem: 'Invalid accept header'
4747
resolution: '%{message}'
48+
invalid_version_header:
49+
problem: 'Invalid version header'
50+
resolution: '%{message}'
4851

lib/grape/middleware/error.rb

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def find_handler(klass)
4747
end
4848

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

lib/grape/middleware/versioner/header.rb

+24-6
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,26 @@ def before
3030

3131
if media_type
3232
media_type_header_handler
33-
elsif headers_contain_wrong_vendor_or_version?
34-
fail_with_invalid_accept_header!('API vendor or version not found.')
33+
elsif headers_contain_wrong_vendor?
34+
fail_with_invalid_accept_header!('API vendor not found.')
35+
elsif headers_contain_wrong_version?
36+
fail_with_invalid_version_header!('API version not found.')
3537
end
3638
end
3739

3840
private
3941

4042
def strict_header_checks
4143
strict_accept_header_presence_check
42-
strict_verion_vendor_accept_header_presence_check
44+
strict_version_vendor_accept_header_presence_check
4345
end
4446

4547
def strict_accept_header_presence_check
4648
return unless header.qvalues.empty?
4749
fail_with_invalid_accept_header!('Accept header must be set.')
4850
end
4951

50-
def strict_verion_vendor_accept_header_presence_check
52+
def strict_version_vendor_accept_header_presence_check
5153
return unless versions.present?
5254
return if an_accept_header_with_version_and_vendor_is_present?
5355
fail_with_invalid_accept_header!('API vendor or version not found.')
@@ -85,6 +87,11 @@ def fail_with_invalid_accept_header!(message)
8587
.new(message, error_headers)
8688
end
8789

90+
def fail_with_invalid_version_header!(message)
91+
fail Grape::Exceptions::InvalidVersionHeader
92+
.new(message, error_headers)
93+
end
94+
8895
def available_media_types
8996
available_media_types = []
9097

@@ -107,9 +114,15 @@ def available_media_types
107114
available_media_types.flatten
108115
end
109116

110-
def headers_contain_wrong_vendor_or_version?
117+
def headers_contain_wrong_vendor?
118+
header.values.all? do |header_value|
119+
vendor?(header_value) && request_vendor(header_value) != vendor
120+
end
121+
end
122+
123+
def headers_contain_wrong_version?
111124
header.values.all? do |header_value|
112-
vendor?(header_value) || version?(header_value)
125+
version?(header_value)
113126
end
114127
end
115128

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

175+
def request_vendor(media_type)
176+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
177+
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
178+
end
179+
162180
# @param [String] media_type a content type
163181
# @return [Boolean] whether the content type sets an API version
164182
def version?(media_type)

spec/grape/exceptions/invalid_accept_header_spec.rb

-14
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,6 @@ def app
5454
end
5555

5656
context 'that receives' do
57-
context 'an invalid version in the request' do
58-
before do
59-
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
60-
'CONTENT_TYPE' => 'application/json'
61-
end
62-
it_should_behave_like 'a rescued request'
63-
end
6457
context 'an invalid vendor in the request' do
6558
before do
6659
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
@@ -128,13 +121,6 @@ def app
128121
end
129122

130123
context 'that receives' do
131-
context 'an invalid version in the request' do
132-
before do
133-
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
134-
'CONTENT_TYPE' => 'application/json'
135-
end
136-
it_should_behave_like 'a rescued request'
137-
end
138124
context 'an invalid vendor in the request' do
139125
before do
140126
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',

spec/grape/middleware/versioner/header_spec.rb

+56-6
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
9090
expect(exception.headers).to eql('X-Cascade' => 'pass')
9191
expect(exception.status).to eql 406
92-
expect(exception.message).to include 'API vendor or version not found'
92+
expect(exception.message).to include 'API vendor not found'
9393
end
9494
end
9595

@@ -116,7 +116,7 @@
116116
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
117117
expect(exception.headers).to eql('X-Cascade' => 'pass')
118118
expect(exception.status).to eql 406
119-
expect(exception.message).to include('API vendor or version not found')
119+
expect(exception.message).to include('API vendor not found')
120120
end
121121
end
122122
end
@@ -141,10 +141,10 @@
141141

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

245245
it 'fails with another version' do
246246
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception|
247-
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
247+
expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
248248
expect(exception.headers).to eql('X-Cascade' => 'pass')
249249
expect(exception.status).to eql 406
250-
expect(exception.message).to include('API vendor or version not found')
250+
expect(exception.message).to include('API version not found')
251+
end
252+
end
253+
end
254+
255+
context 'when there are multiple versions specified with rescue_from :all' do
256+
subject {
257+
Class.new(Grape::API) do
258+
rescue_from :all
259+
end
260+
}
261+
262+
let(:v1_app) {
263+
Class.new(Grape::API) do
264+
version 'v1', using: :header, vendor: 'test'
265+
resources :users do
266+
get :hello do
267+
'one'
268+
end
269+
end
270+
end
271+
}
272+
273+
let(:v2_app) {
274+
Class.new(Grape::API) do
275+
version 'v2', using: :header, vendor: 'test'
276+
resources :users do
277+
get :hello do
278+
'two'
279+
end
280+
end
281+
end
282+
}
283+
284+
def app
285+
subject.mount v1_app
286+
subject.mount v2_app
287+
subject
288+
end
289+
290+
context 'with header versioned endpoints and a rescue_all block defined' do
291+
it 'responds correctly to a v1 request' do
292+
versioned_get '/users/hello', 'v1', using: :header, vendor: 'test'
293+
expect(last_response.body).to eq('one')
294+
expect(last_response.body).not_to include('API vendor or version not found')
295+
end
296+
297+
it 'responds correctly to a v2 request' do
298+
versioned_get '/users/hello', 'v2', using: :header, vendor: 'test'
299+
expect(last_response.body).to eq('two')
300+
expect(last_response.body).not_to include('API vendor or version not found')
251301
end
252302
end
253303
end

0 commit comments

Comments
 (0)