Skip to content

Commit c439c31

Browse files
author
elliotlarson
committed
incorrect version accept header does not return a 406 response
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 e23218f commit c439c31

File tree

3 files changed

+93
-43
lines changed

3 files changed

+93
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [#1052](https://github.com/ruby-grape/grape/pull/1052): Reset `description[:params]` when resetting validations - [@marshall-lee](https://github.com/marshall-lee).
2525
* [#1088](https://github.com/ruby-grape/grape/pull/1088): Support ActiveSupport 3.x by explicitly requiring `Hash#except` - [@wagenet](https://github.com/wagenet).
2626
* [#1096](https://github.com/ruby-grape/grape/pull/1096): Fix coercion on booleans - [@towanda](https://github.com/towanda).
27+
* [#1101](https://github.com/intridea/grape/pull/1101): With strict version header, 406 response when using media type accept header - [@elliotlarson](https://github.com/elliotlarson).
2728

2829
0.12.0 (6/18/2015)
2930
==================

lib/grape/middleware/versioner/header.rb

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

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
42-
end
43-
44-
media_type = header.best_of available_media_types
28+
def before
29+
strict_header_checks if strict?
4530

4631
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
32+
media_type_header_handler
5633
# If none of the available content types are acceptable:
5734
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)
35+
fail_with_invalid_accept_header!('406 Not Acceptable')
36+
elsif headers_contain_wrong_vendor_or_version?
37+
fail_with_invalid_accept_header!('API vendor or version not found.')
6238
end
6339
end
6440

6541
private
6642

43+
def strict_header_checks
44+
# If no Accept header:
45+
if header.qvalues.empty?
46+
fail_with_invalid_accept_header!('Accept header must be set.')
47+
end
48+
# Remove any acceptable content types with ranges.
49+
header.qvalues.reject! do |media_type, _|
50+
Rack::Accept::Header.parse_media_type(media_type).find do |s|
51+
s == '*'
52+
end
53+
end
54+
# If all Accept headers included a range:
55+
if header.qvalues.empty?
56+
fail_with_invalid_accept_header!(
57+
'Accept header must not contain ranges ("*").'
58+
)
59+
end
60+
end
61+
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+
elsif versions.size > 0
81+
fail_with_invalid_accept_header!(
82+
'API vendor or version not found.'
83+
)
84+
end
85+
end
86+
87+
def fail_with_invalid_accept_header!(message)
88+
fail Grape::Exceptions::InvalidAcceptHeader
89+
.new(message, error_headers)
90+
end
91+
6792
def available_media_types
6893
available_media_types = []
6994

7095
content_types.each do |extension, _media_type|
7196
versions.reverse_each do |version|
72-
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
97+
available_media_types += [
98+
"application/vnd.#{vendor}-#{version}+#{extension}",
99+
"application/vnd.#{vendor}-#{version}"
100+
]
73101
end
74102
available_media_types << "application/vnd.#{vendor}+#{extension}"
75103
end
@@ -83,30 +111,42 @@ def available_media_types
83111
available_media_types.flatten
84112
end
85113

114+
def headers_contain_wrong_vendor_or_version?
115+
header.values.all? do |header_value|
116+
has_vendor?(header_value) || version?(header_value)
117+
end
118+
end
119+
86120
def rack_accept_header
87121
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
88122
rescue RuntimeError => e
89-
raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers)
123+
fail_with_invalid_accept_header!(e.message)
90124
end
91125

92126
def versions
93127
options[:versions] || []
94128
end
95129

96130
def vendor
97-
options[:version_options] && options[:version_options][:vendor]
131+
version_options && version_options[:vendor]
98132
end
99133

100134
def strict?
101-
options[:version_options] && options[:version_options][:strict]
135+
version_options && version_options[:strict]
136+
end
137+
138+
def version_options
139+
options[:version_options]
102140
end
103141

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`.
142+
# By default those errors contain an `X-Cascade` header set to `pass`,
143+
# which allows nesting and stacking of routes
144+
# (see [Rack::Mount](https://github.com/josh/rack-mount) for more
145+
# information). To prevent # this behavior, and not add the `X-Cascade`
146+
# header, one can set the `:cascade` option to `false`.
107147
def cascade?
108-
if options[:version_options] && options[:version_options].key?(:cascade)
109-
!!options[:version_options][:cascade]
148+
if version_options && version_options.key?(:cascade)
149+
!!version_options[:cascade]
110150
else
111151
true
112152
end
@@ -119,14 +159,14 @@ def error_headers
119159
# @param [String] media_type a content type
120160
# @return [Boolean] whether the content type sets a vendor
121161
def has_vendor?(media_type)
122-
_, subtype = Rack::Accept::Header.parse_media_type media_type
162+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
123163
subtype[/\Avnd\.[a-z0-9*.]+/]
124164
end
125165

126166
# @param [String] media_type a content type
127167
# @return [Boolean] whether the content type sets an API version
128168
def version?(media_type)
129-
_, subtype = Rack::Accept::Header.parse_media_type media_type
169+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
130170
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
131171
end
132172
end

spec/grape/middleware/versioner/header_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,15 @@
223223
end
224224
end
225225

226+
it 'fails with 406 Not Acceptable if header is application/xml' do
227+
expect { subject.call('HTTP_ACCEPT' => 'application/xml').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('API vendor or version not found.')
232+
end
233+
end
234+
226235
it 'fails with 406 Not Acceptable if header is empty' do
227236
expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
228237
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)

0 commit comments

Comments
 (0)