Skip to content

Commit c93f12c

Browse files
committed
Add rack_request to Grape::Middleware::Base
Add some rescues when dealing with rack_params and GET functions Add exceptions related to rescues
1 parent 2b35fed commit c93f12c

File tree

11 files changed

+172
-34
lines changed

11 files changed

+172
-34
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module Exceptions
5+
class ConflictingTypes < Base
6+
def initialize
7+
super(message: compose_message(:conflicting_types), status: 400)
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module Exceptions
5+
class InvalidParameters < Base
6+
def initialize
7+
super(message: compose_message(:invalid_parameters), status: 400)
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module Exceptions
5+
class TooDeepParameters < Base
6+
def initialize(limit)
7+
super(message: compose_message(:too_deep_parameters, limit: limit), status: 400)
8+
end
9+
end
10+
end
11+
end

lib/grape/locale/en.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,6 @@ en:
5858
problem: 'invalid version header'
5959
resolution: '%{message}'
6060
invalid_response: 'Invalid response'
61-
query_parsing: 'query params are not parsable'
61+
conflicting_types: 'query params contains conflicting types'
62+
invalid_parameters: 'query params contains invalid format or byte sequence'
63+
too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})'

lib/grape/middleware/base.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ class Base
66
include Helpers
77
include Grape::DSL::Headers
88

9-
attr_reader :app, :env, :options
10-
11-
TEXT_HTML = 'text/html'
9+
attr_reader :app, :env, :options, :rack_request
1210

1311
# @param [Rack Application] app The standard argument for a Rack middleware.
1412
# @param [Hash] options A hash of options, simply stored for use by subclasses.
@@ -54,6 +52,10 @@ def before; end
5452
# @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
5553
def after; end
5654

55+
def rack_request
56+
@rack_request ||= Rack::Request.new(env)
57+
end
58+
5759
def response
5860
return @app_response if @app_response.is_a?(Rack::Response)
5961

@@ -73,7 +75,15 @@ def content_type_for(format)
7375
end
7476

7577
def content_type
76-
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
78+
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html'
79+
end
80+
81+
def query_params
82+
@rack_request.GET
83+
rescue Rack::QueryParser::ParamsTooDeepError
84+
raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
85+
rescue Rack::Utils::ParameterTypeError
86+
raise Grape::Exceptions::ConflictingTypes
7787
end
7888

7989
private

lib/grape/middleware/error.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def call!(env)
3939
private
4040

4141
def rack_response(status, headers, message)
42-
message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML
42+
message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
4343
Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
4444
end
4545

lib/grape/middleware/formatter.rb

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
module Grape
44
module Middleware
55
class Formatter < Base
6-
CHUNKED = 'chunked'
7-
FORMAT = 'format'
86

97
def default_options
108
{
@@ -69,34 +67,27 @@ def ensure_content_type(headers)
6967
end
7068
end
7169

72-
def request
73-
@request ||= Rack::Request.new(env)
74-
end
75-
76-
# store read input in env['api.request.input']
7770
def read_body_input
78-
return unless
79-
(request.post? || request.put? || request.patch? || request.delete?) &&
80-
(!request.form_data? || !request.media_type) &&
81-
!request.parseable_data? &&
82-
(request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
83-
84-
return unless (input = env[Rack::RACK_INPUT])
71+
input = rack_request.body # reads RACK_INPUT
72+
return if input.nil?
73+
return unless read_body_input?
8574

8675
input.try(:rewind)
8776
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
8877
begin
89-
read_rack_input(body) if body && !body.empty?
78+
read_rack_input(body)
9079
ensure
9180
input.try(:rewind)
9281
end
9382
end
9483

95-
# store parsed input in env['api.request.body']
9684
def read_rack_input(body)
97-
fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
85+
return if body.empty?
86+
87+
media_type = rack_request.media_type
88+
fmt = media_type ? mime_types[media_type] : options[:default_format]
9889

99-
throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
90+
throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
10091
parser = Grape::Parser.parser_for fmt, options[:parsers]
10192
if parser
10293
begin
@@ -119,8 +110,21 @@ def read_rack_input(body)
119110
end
120111
end
121112

113+
# this middleware will not try to format the following content-types since Rack already handles them
114+
# when calling Rack's `params` function
115+
# - application/x-www-form-urlencoded
116+
# - multipart/form-data
117+
# - multipart/related
118+
# - multipart/mixed
119+
def read_body_input?
120+
(rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
121+
!(rack_request.form_data? && rack_request.content_type) &&
122+
!rack_request.parseable_data? &&
123+
(rack_request.content_length.to_i.positive? || rack_request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == 'chunked')
124+
end
125+
122126
def negotiate_content_type
123-
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
127+
fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
124128
if content_type_for(fmt)
125129
env[Grape::Env::API_FORMAT] = fmt.to_sym
126130
else
@@ -129,18 +133,14 @@ def negotiate_content_type
129133
end
130134

131135
def format_from_extension
132-
request_path = request.path.try(:scrub)
136+
request_path = rack_request.path.try(:scrub)
133137
dot_pos = request_path.rindex('.')
134138
return unless dot_pos
135139

136140
extension = request_path[dot_pos + 1..]
137141
extension if content_type_for(extension)
138142
end
139143

140-
def format_from_params
141-
Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT]
142-
end
143-
144144
def format_from_header
145145
accept_header = env[Grape::Http::Headers::HTTP_ACCEPT].try(:scrub)
146146
return if accept_header.blank?

lib/grape/middleware/versioner/param.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ module Versioner
2020
# env['api.version'] => 'v1'
2121
class Param < Base
2222
def before
23-
potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key]
23+
potential_version = query_params[parameter_key]
2424
return if potential_version.blank?
2525

2626
version_not_found! unless potential_version_match?(potential_version)
27-
env[Grape::Env::API_VERSION] = potential_version
28-
env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH
27+
env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key)
2928
end
3029
end
3130
end

lib/grape/request.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ def make_params
3737
@params_builder.call(rack_params).deep_merge!(grape_routing_args)
3838
rescue EOFError
3939
raise Grape::Exceptions::EmptyMessageBody.new(content_type)
40-
rescue Rack::Multipart::MultipartPartLimitError
40+
rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError
4141
raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
42+
rescue Rack::QueryParser::ParamsTooDeepError
43+
raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
44+
rescue Rack::Utils::ParameterTypeError
45+
raise Grape::Exceptions::ConflictingTypes
46+
rescue Rack::Utils::InvalidParameterError
47+
raise Grape::Exceptions::InvalidParameters
4248
end
4349

4450
def build_headers

spec/grape/middleware/base_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,20 @@ def after
223223
expect(last_response.headers['X-Test-Overwriting']).to eq('Bye')
224224
end
225225
end
226+
227+
describe 'query_params' do
228+
context 'when query params are conflicting' do
229+
it 'raises an ConflictingTypes error' do
230+
expect { get '/?x[y]=1&x[y]z=2' } .to raise_error(Grape::Exceptions::ConflictingTypes)
231+
end
232+
end
233+
234+
context 'when query params is over the specified limit' do
235+
let(:query_params) { "foo#{"[a]" * Rack::Utils.param_depth_limit}=bar" }
236+
237+
it 'raises an ConflictingTypes error' do
238+
expect { get "/?foo#{"[a]" * Rack::Utils.param_depth_limit}=bar" } .to raise_error(Grape::Exceptions::TooDeepParameters)
239+
end
240+
end
241+
end
226242
end

0 commit comments

Comments
 (0)