Skip to content

Commit d7718f0

Browse files
elliotlarsondblock
elliotlarson
authored andcommitted
Fix: Incorrect media-type Accept header now correctly returns 406 with strict: true.
If you've configured Grape to use accept header versioning with strict equal to true, Grape should return a 406 response when making a request with an incorrect version header. However, the system was not responding with a 406 when using the header 'Accept:application/xml'. Adding relevant line to changelog. Refactoring the middleware version header class: - Breaking up the before method into more manageable pieces, removing some of the conditional (cyclomatic) complexity - fixing some line lengths - removing some local variables from methods
1 parent 4b6e2b6 commit d7718f0

File tree

3 files changed

+88
-78
lines changed

3 files changed

+88
-78
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#### Fixes
99

1010
* [#1109](https://github.com/ruby-grape/grape/pull/1109): Memoize Virtus attribute and fix memory leak - [@marshall-lee](https://github.com/marshall-lee).
11-
* Your contribution here.
11+
* [#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).
1212

1313
0.13.0 (8/10/2015)
1414
==================

lib/grape/middleware/versioner/header.rb

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,54 +22,78 @@ module Versioner
2222
# X-Cascade header to alert Rack::Mount to attempt the next matched
2323
# route.
2424
class Header < Base
25+
VENDOR_VERSION_HEADER_REGEX =
26+
/\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/
27+
2528
def before
26-
header = rack_accept_header
29+
strict_header_checks if strict?
2730

28-
if strict?
29-
# If no Accept header:
30-
if header.qvalues.empty?
31-
fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must be set.', error_headers)
32-
end
33-
# Remove any acceptable content types with ranges.
34-
header.qvalues.reject! do |media_type, _|
35-
Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
36-
end
37-
# If all Accept headers included a range:
38-
if header.qvalues.empty?
39-
fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must not contain ranges ("*").',
40-
error_headers)
41-
end
31+
if media_type
32+
media_type_header_handler
33+
elsif headers_contain_wrong_vendor_or_version?
34+
fail_with_invalid_accept_header!('API vendor or version not found.')
4235
end
36+
end
4337

44-
media_type = header.best_of available_media_types
38+
private
4539

46-
if media_type
47-
type, subtype = Rack::Accept::Header.parse_media_type media_type
48-
env['api.type'] = type
49-
env['api.subtype'] = subtype
50-
51-
if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype
52-
env['api.vendor'] = Regexp.last_match[1]
53-
env['api.version'] = Regexp.last_match[2]
54-
env['api.format'] = Regexp.last_match[3] # weird that Grape::Middleware::Formatter also does this
55-
end
56-
# If none of the available content types are acceptable:
57-
elsif strict?
58-
fail Grape::Exceptions::InvalidAcceptHeader.new('406 Not Acceptable', error_headers)
59-
# If all acceptable content types specify a vendor or version that doesn't exist:
60-
elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
61-
fail Grape::Exceptions::InvalidAcceptHeader.new('API vendor or version not found.', error_headers)
40+
def strict_header_checks
41+
strict_accept_header_presence_check
42+
strict_verion_vendor_accept_header_presence_check
43+
end
44+
45+
def strict_accept_header_presence_check
46+
return unless header.qvalues.empty?
47+
fail_with_invalid_accept_header!('Accept header must be set.')
48+
end
49+
50+
def strict_verion_vendor_accept_header_presence_check
51+
return unless versions.present?
52+
return if an_accept_header_with_version_and_vendor_is_present?
53+
fail_with_invalid_accept_header!('API vendor or version not found.')
54+
end
55+
56+
def an_accept_header_with_version_and_vendor_is_present?
57+
header.qvalues.keys.any? do |h|
58+
VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
6259
end
6360
end
6461

65-
private
62+
def header
63+
@header ||= rack_accept_header
64+
end
65+
66+
def media_type
67+
@media_type ||= header.best_of(available_media_types)
68+
end
69+
70+
def media_type_header_handler
71+
type, subtype = Rack::Accept::Header.parse_media_type(media_type)
72+
env['api.type'] = type
73+
env['api.subtype'] = subtype
74+
75+
if VENDOR_VERSION_HEADER_REGEX =~ subtype
76+
env['api.vendor'] = Regexp.last_match[1]
77+
env['api.version'] = Regexp.last_match[2]
78+
# weird that Grape::Middleware::Formatter also does this
79+
env['api.format'] = Regexp.last_match[3]
80+
end
81+
end
82+
83+
def fail_with_invalid_accept_header!(message)
84+
fail Grape::Exceptions::InvalidAcceptHeader
85+
.new(message, error_headers)
86+
end
6687

6788
def available_media_types
6889
available_media_types = []
6990

7091
content_types.each do |extension, _media_type|
7192
versions.reverse_each do |version|
72-
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
93+
available_media_types += [
94+
"application/vnd.#{vendor}-#{version}+#{extension}",
95+
"application/vnd.#{vendor}-#{version}"
96+
]
7397
end
7498
available_media_types << "application/vnd.#{vendor}+#{extension}"
7599
end
@@ -83,30 +107,42 @@ def available_media_types
83107
available_media_types.flatten
84108
end
85109

110+
def headers_contain_wrong_vendor_or_version?
111+
header.values.all? do |header_value|
112+
has_vendor?(header_value) || version?(header_value)
113+
end
114+
end
115+
86116
def rack_accept_header
87117
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
88118
rescue RuntimeError => e
89-
raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers)
119+
fail_with_invalid_accept_header!(e.message)
90120
end
91121

92122
def versions
93123
options[:versions] || []
94124
end
95125

96126
def vendor
97-
options[:version_options] && options[:version_options][:vendor]
127+
version_options && version_options[:vendor]
98128
end
99129

100130
def strict?
101-
options[:version_options] && options[:version_options][:strict]
131+
version_options && version_options[:strict]
132+
end
133+
134+
def version_options
135+
options[:version_options]
102136
end
103137

104-
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
105-
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
106-
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
138+
# By default those errors contain an `X-Cascade` header set to `pass`,
139+
# which allows nesting and stacking of routes
140+
# (see [Rack::Mount](https://github.com/josh/rack-mount) for more
141+
# information). To prevent # this behavior, and not add the `X-Cascade`
142+
# header, one can set the `:cascade` option to `false`.
107143
def cascade?
108-
if options[:version_options] && options[:version_options].key?(:cascade)
109-
!!options[:version_options][:cascade]
144+
if version_options && version_options.key?(:cascade)
145+
!!version_options[:cascade]
110146
else
111147
true
112148
end
@@ -119,14 +155,14 @@ def error_headers
119155
# @param [String] media_type a content type
120156
# @return [Boolean] whether the content type sets a vendor
121157
def has_vendor?(media_type)
122-
_, subtype = Rack::Accept::Header.parse_media_type media_type
158+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
123159
subtype[/\Avnd\.[a-z0-9*.]+/]
124160
end
125161

126162
# @param [String] media_type a content type
127163
# @return [Boolean] whether the content type sets an API version
128164
def version?(media_type)
129-
_, subtype = Rack::Accept::Header.parse_media_type media_type
165+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
130166
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
131167
end
132168
end

spec/grape/middleware/versioner/header_spec.rb

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -184,24 +184,6 @@
184184
end
185185
end
186186

187-
it 'fails with 406 Not Acceptable if type is a range' do
188-
expect { subject.call('HTTP_ACCEPT' => '*/*').last }.to raise_exception do |exception|
189-
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
190-
expect(exception.headers).to eql('X-Cascade' => 'pass')
191-
expect(exception.status).to eql 406
192-
expect(exception.message).to include('Accept header must not contain ranges ("*").')
193-
end
194-
end
195-
196-
it 'fails with 406 Not Acceptable if subtype is a range' do
197-
expect { subject.call('HTTP_ACCEPT' => 'application/*').last }.to raise_exception do |exception|
198-
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
199-
expect(exception.headers).to eql('X-Cascade' => 'pass')
200-
expect(exception.status).to eql 406
201-
expect(exception.message).to include('Accept header must not contain ranges ("*").')
202-
end
203-
end
204-
205187
it 'succeeds if proper header is set' do
206188
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
207189
end
@@ -223,30 +205,22 @@
223205
end
224206
end
225207

226-
it 'fails with 406 Not Acceptable if header is empty' do
227-
expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
228-
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
229-
expect(exception.headers).to eql({})
230-
expect(exception.status).to eql 406
231-
expect(exception.message).to include('Accept header must be set.')
232-
end
233-
end
234-
235-
it 'fails with 406 Not Acceptable if type is a range' do
236-
expect { subject.call('HTTP_ACCEPT' => '*/*').last }.to raise_exception do |exception|
208+
it 'fails with 406 Not Acceptable if header is application/xml' do
209+
expect { subject.call('HTTP_ACCEPT' => 'application/xml').last }
210+
.to raise_exception do |exception|
237211
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
238212
expect(exception.headers).to eql({})
239213
expect(exception.status).to eql 406
240-
expect(exception.message).to include('Accept header must not contain ranges ("*").')
214+
expect(exception.message).to include('API vendor or version not found.')
241215
end
242216
end
243217

244-
it 'fails with 406 Not Acceptable if subtype is a range' do
245-
expect { subject.call('HTTP_ACCEPT' => 'application/*').last }.to raise_exception do |exception|
218+
it 'fails with 406 Not Acceptable if header is empty' do
219+
expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
246220
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
247221
expect(exception.headers).to eql({})
248222
expect(exception.status).to eql 406
249-
expect(exception.message).to include('Accept header must not contain ranges ("*").')
223+
expect(exception.message).to include('Accept header must be set.')
250224
end
251225
end
252226

0 commit comments

Comments
 (0)