grape 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +52 -18
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -2
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
|
@@ -13,12 +13,60 @@ module Grape
|
|
|
13
13
|
|
|
14
14
|
ALL_MEDIA_TYPES = '*/*'
|
|
15
15
|
|
|
16
|
+
# The query param that names the format. See #format_from_query.
|
|
17
|
+
FORMAT_PARAM = 'format'
|
|
18
|
+
|
|
19
|
+
# @api private
|
|
20
|
+
# The format an Accept header asks for out of +mime_types+, or nil.
|
|
21
|
+
# Callers scrub the header first.
|
|
22
|
+
#
|
|
23
|
+
# Media types are case-insensitive (RFC 9110 §8.3.1) but the registered
|
|
24
|
+
# ones are spelled in lower case and Rack matches them literally, so an
|
|
25
|
+
# `Accept: TEXT/PLAIN` found nothing and fell through to the default
|
|
26
|
+
# format — the client quietly got something other than what it asked for.
|
|
27
|
+
def self.format_for_accept(accept_header, mime_types)
|
|
28
|
+
return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
|
|
29
|
+
|
|
30
|
+
media_type = Rack::Utils.best_q_match(accept_header.downcase, mime_types.keys)
|
|
31
|
+
mime_types[media_type] if media_type
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# +format_for_accept+ answers from the header and +mime_types+ alone. A
|
|
35
|
+
# client that names the format it wants sends one of those media types as
|
|
36
|
+
# it is registered (+application/json+), and most others send +*/*+ or
|
|
37
|
+
# nothing, so those answers are worked out once instead of re-running
|
|
38
|
+
# Rack's q-value match on every request. Any other header misses and is
|
|
39
|
+
# negotiated as before; only registered media types are keys, so a client
|
|
40
|
+
# cannot grow a table.
|
|
41
|
+
#
|
|
42
|
+
# Shared per +mime_types+, which Grape::ContentTypes already shares per
|
|
43
|
+
# content-type registry: every endpoint builds its own formatter.
|
|
44
|
+
class FormatForAcceptCache < Grape::Util::Cache
|
|
45
|
+
def initialize
|
|
46
|
+
super
|
|
47
|
+
@cache = Hash.new do |h, mime_types|
|
|
48
|
+
h[mime_types] = [*mime_types.keys, ALL_MEDIA_TYPES, nil].to_h { |accept| [accept, Formatter.format_for_accept(accept, mime_types)] }.freeze
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
16
53
|
# The request methods that can carry a body worth parsing. See
|
|
17
|
-
# {#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
54
|
+
# {#body_given?}, which tests the env against this before anything asks
|
|
55
|
+
# for a Rack::Request. QUERY is here because its content *is* the query
|
|
56
|
+
# (RFC 10008, Section 2), not an optional payload.
|
|
20
57
|
BODY_CARRYING_METHODS = [Rack::POST, Rack::PUT, Rack::PATCH, Rack::DELETE, Grape::QUERY].freeze
|
|
21
58
|
|
|
59
|
+
# The media types whose bodies Rack parses itself once the params are
|
|
60
|
+
# asked for, so the formatter leaves them alone.
|
|
61
|
+
#
|
|
62
|
+
# Tested against the media type the formatter parses anyway, rather than
|
|
63
|
+
# through Rack's +form_data?+ and +parseable_data?+, which parse
|
|
64
|
+
# CONTENT_TYPE again each. It is the same answer: +form_data?+ also holds
|
|
65
|
+
# for a POST with no media type, but Rack only finds none when
|
|
66
|
+
# CONTENT_TYPE is missing or empty, and a body like that was never left
|
|
67
|
+
# to Rack.
|
|
68
|
+
RACK_PARSED_MEDIA_TYPES = (Rack::Request::FORM_DATA_MEDIA_TYPES + Rack::Request::PARSEABLE_DATA_MEDIA_TYPES).freeze
|
|
69
|
+
|
|
22
70
|
# Read off ivars rather than delegated into +config+ on every request:
|
|
23
71
|
# +negotiate_content_type+ asks for +format+ and +default_format+ per
|
|
24
72
|
# request and +fetch_formatter+ for +formatters+, and each delegator cost
|
|
@@ -36,60 +84,115 @@ module Grape
|
|
|
36
84
|
@formatters = config.formatters
|
|
37
85
|
@parsers = config.parsers
|
|
38
86
|
mime_types
|
|
87
|
+
# Only an API that pins no format negotiates one from the Accept header.
|
|
88
|
+
@format_for_accept = FormatForAcceptCache[mime_types] unless format
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Base#call copies the middleware per request so #before and #after can
|
|
92
|
+
# keep the env and the app's response in ivars. The formatter keeps
|
|
93
|
+
# neither: it passes the env along and answers from the one instance the
|
|
94
|
+
# stack built.
|
|
95
|
+
def call(env)
|
|
96
|
+
call!(env).to_a
|
|
39
97
|
end
|
|
40
98
|
|
|
41
|
-
def
|
|
42
|
-
negotiate_content_type
|
|
43
|
-
read_body_input
|
|
99
|
+
def call!(env)
|
|
100
|
+
negotiated = negotiate_content_type(env)
|
|
101
|
+
read_body_input(env)
|
|
102
|
+
response = @app.call(env)
|
|
103
|
+
reporting_after_errors { format_response(env, response, negotiated) } || response
|
|
44
104
|
end
|
|
45
105
|
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
# Base's per-request readers look for the request in ivars that only a
|
|
107
|
+
# per-request copy sets, so the one instance the formatter answers from
|
|
108
|
+
# has none to give: +env+ is nil, +response+ raises, and +rack_request+
|
|
109
|
+
# would memoize a request on the instance every request shares.
|
|
110
|
+
private :env, :context, :rack_request, :query_params, :response
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def format_response(env, response, negotiated)
|
|
115
|
+
return unless response
|
|
48
116
|
|
|
49
|
-
status, headers, bodies =
|
|
117
|
+
status, headers, bodies = response
|
|
50
118
|
|
|
51
119
|
return [status, headers, []] if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
|
52
120
|
|
|
53
|
-
build_formatted_response(status, headers, bodies)
|
|
121
|
+
build_formatted_response(env, status, headers, bodies, negotiated)
|
|
54
122
|
end
|
|
55
123
|
|
|
56
|
-
|
|
124
|
+
def build_formatted_response(env, status, headers, bodies, negotiated)
|
|
125
|
+
content_type_given = headers.key?(Rack::CONTENT_TYPE)
|
|
126
|
+
ensure_content_type!(env, headers)
|
|
127
|
+
return stream_response(status, headers, bodies) if bodies.is_a?(Grape::ServeStream::StreamResponse)
|
|
128
|
+
|
|
129
|
+
bodymap = format_bodies(env, headers, bodies)
|
|
130
|
+
# A formatter that renders a body only as Rack reads it (see
|
|
131
|
+
# Grape::Formatter::Jsonl) hands back a stream, served as +stream+'s is.
|
|
132
|
+
return stream_response(status, headers, bodymap.first) if bodymap.first.is_a?(Grape::ServeStream::StreamResponse)
|
|
133
|
+
|
|
134
|
+
# A bare Rack tuple rather than a Rack::Response: +headers+ is already
|
|
135
|
+
# a Grape::Util::Header (a Rack::Headers on Rack 3), so wrapping only
|
|
136
|
+
# re-normalizes the same keys into a second Headers hash that
|
|
137
|
+
# +Middleware::Base#call+ unwraps again with +to_a+ on the way out.
|
|
138
|
+
# The 204/304 bodies Rack::Response#finish would blank are returned
|
|
139
|
+
# by #format_response, before this point.
|
|
140
|
+
[status, headers, bodymap]
|
|
141
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
|
142
|
+
fallback = format_as_default(env, status, headers, bodies) if falls_back?(env, negotiated, content_type_given)
|
|
143
|
+
return fallback if fallback
|
|
57
144
|
|
|
58
|
-
|
|
59
|
-
|
|
145
|
+
throw :error, Grape::Exceptions::ErrorResponse.new(status: 500, message: e.message, backtrace: e.backtrace, original_exception: e)
|
|
146
|
+
end
|
|
60
147
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Allow content-type to be explicitly overwritten
|
|
67
|
-
formatter = fetch_formatter(headers)
|
|
68
|
-
bodymap = instrument_format_response(formatter) do
|
|
69
|
-
bodies.map { |body| formatter.call(body, env) }
|
|
70
|
-
end
|
|
71
|
-
# A bare Rack tuple rather than a Rack::Response: +headers+ is already
|
|
72
|
-
# a Grape::Util::Header (a Rack::Headers on Rack 3), so wrapping only
|
|
73
|
-
# re-normalizes the same keys into a second Headers hash that
|
|
74
|
-
# +Middleware::Base#call+ unwraps again with +to_a+ on the way out.
|
|
75
|
-
# The 204/304 bodies Rack::Response#finish would blank are returned
|
|
76
|
-
# above, before this point.
|
|
77
|
-
[status, headers, bodymap]
|
|
148
|
+
def format_bodies(env, headers, bodies)
|
|
149
|
+
# Picked by env['api.format']; the Content-Type header only counts when that is unset
|
|
150
|
+
formatter = fetch_formatter(env, headers)
|
|
151
|
+
instrument_format_response(env, formatter) do
|
|
152
|
+
bodies.map { |body| formatter.call(body, env) }
|
|
78
153
|
end
|
|
79
|
-
|
|
80
|
-
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def stream_response(status, headers, bodies)
|
|
157
|
+
Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
|
|
158
|
+
resp.body = bodies.stream
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A format the client negotiated -- by its Accept header, the path's
|
|
163
|
+
# extension or +?format=+ -- that cannot render the body answers in the
|
|
164
|
+
# default format rather than 500. On an API that pins no format, a
|
|
165
|
+
# browser's Accept header and axios's both negotiate XML, which cannot
|
|
166
|
+
# render a String. The fallback applies only while the format is the
|
|
167
|
+
# client's alone: when the API pins one, or the endpoint set a
|
|
168
|
+
# Content-Type or another format itself, a body that does not fit is the
|
|
169
|
+
# endpoint's error and still answers 500.
|
|
170
|
+
def falls_back?(env, negotiated, content_type_given)
|
|
171
|
+
format.nil? && !content_type_given && env[Grape::Env::API_FORMAT] == negotiated
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Nil when the default format cannot render the body either.
|
|
175
|
+
def format_as_default(env, status, headers, bodies)
|
|
176
|
+
formatter = Grape::Formatter.formatter_for(default_format, formatters)
|
|
177
|
+
bodymap = bodies.map { |body| formatter.call(body, env) }
|
|
178
|
+
env[Grape::Env::API_FORMAT] = default_format.to_sym
|
|
179
|
+
headers.delete(Rack::CONTENT_TYPE)
|
|
180
|
+
ensure_content_type!(env, headers)
|
|
181
|
+
[status, headers, bodymap]
|
|
182
|
+
rescue Grape::Exceptions::InvalidFormatter
|
|
183
|
+
nil
|
|
81
184
|
end
|
|
82
185
|
|
|
83
186
|
# Guards on +listening?+ so that with no subscriber the payload Hash and
|
|
84
187
|
# notification machinery are skipped and the block runs directly (no added
|
|
85
188
|
# allocations); the block is forwarded anonymously.
|
|
86
|
-
def instrument_format_response(formatter, &)
|
|
189
|
+
def instrument_format_response(env, formatter, &)
|
|
87
190
|
return yield unless ActiveSupport::Notifications.notifier.listening?('format_response.grape')
|
|
88
191
|
|
|
89
192
|
ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:, &)
|
|
90
193
|
end
|
|
91
194
|
|
|
92
|
-
def fetch_formatter(headers)
|
|
195
|
+
def fetch_formatter(env, headers)
|
|
93
196
|
api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
|
|
94
197
|
Grape::Formatter.formatter_for(api_format, formatters)
|
|
95
198
|
end
|
|
@@ -100,18 +203,26 @@ module Grape
|
|
|
100
203
|
# this runs on every response, and copying the hash costs an allocation
|
|
101
204
|
# per request for a header the caller is about to send anyway.
|
|
102
205
|
#
|
|
206
|
+
# A format with no media type at all (see #media_type_for) leaves the
|
|
207
|
+
# header out: Rack forbids a nil header value.
|
|
208
|
+
#
|
|
103
209
|
# @param headers [Hash] the response headers, mutated in place
|
|
104
210
|
# @return [void]
|
|
105
|
-
def ensure_content_type!(headers)
|
|
211
|
+
def ensure_content_type!(env, headers)
|
|
106
212
|
return if headers[Rack::CONTENT_TYPE]
|
|
107
213
|
|
|
108
|
-
|
|
214
|
+
content_type = media_type_for(env[Grape::Env::API_FORMAT])
|
|
215
|
+
headers[Rack::CONTENT_TYPE] = content_type if content_type
|
|
109
216
|
end
|
|
110
217
|
|
|
111
|
-
def read_body_input
|
|
112
|
-
return unless
|
|
218
|
+
def read_body_input(env)
|
|
219
|
+
return unless body_given?(env)
|
|
220
|
+
|
|
221
|
+
request = Rack::Request.new(env)
|
|
222
|
+
media_type = request.media_type
|
|
223
|
+
return if RACK_PARSED_MEDIA_TYPES.include?(media_type)
|
|
113
224
|
|
|
114
|
-
input =
|
|
225
|
+
input = request.body # reads RACK_INPUT
|
|
115
226
|
return if input.nil?
|
|
116
227
|
|
|
117
228
|
rewind = input.respond_to?(:rewind)
|
|
@@ -119,17 +230,40 @@ module Grape
|
|
|
119
230
|
input.rewind if rewind
|
|
120
231
|
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
|
|
121
232
|
begin
|
|
122
|
-
read_rack_input(body)
|
|
233
|
+
read_rack_input(env, body, media_type)
|
|
123
234
|
ensure
|
|
124
235
|
input.rewind if rewind
|
|
125
236
|
end
|
|
126
237
|
end
|
|
127
238
|
|
|
128
|
-
def read_rack_input(body)
|
|
239
|
+
def read_rack_input(env, body, media_type)
|
|
129
240
|
return if body.empty?
|
|
130
241
|
|
|
131
|
-
|
|
242
|
+
parser = Grape::Parser.parser_for body_format(env, media_type), parsers
|
|
243
|
+
return env[Grape::Env::API_REQUEST_BODY] = body unless parser
|
|
132
244
|
|
|
245
|
+
begin
|
|
246
|
+
parsed = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
|
|
247
|
+
merge_form_hash(env, parsed) if parsed.is_a?(Hash)
|
|
248
|
+
rescue ForeignParserError => e
|
|
249
|
+
throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# A body parsed into a Hash joins the form params Rack::Request reads,
|
|
254
|
+
# as a form body Rack parsed itself would.
|
|
255
|
+
def merge_form_hash(env, parsed)
|
|
256
|
+
if (form_hash = env[Rack::RACK_REQUEST_FORM_HASH])
|
|
257
|
+
form_hash.merge!(parsed)
|
|
258
|
+
else
|
|
259
|
+
env[Rack::RACK_REQUEST_FORM_HASH] = parsed
|
|
260
|
+
end
|
|
261
|
+
env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The format a body of +media_type+ is read as, thrown as an error
|
|
265
|
+
# response when it cannot be read at all.
|
|
266
|
+
def body_format(env, media_type)
|
|
133
267
|
# RFC 10008, Sections 2 and 2.1: a QUERY carries its query in the
|
|
134
268
|
# content, so a request that never says what that content is cannot be
|
|
135
269
|
# interpreted and must fail rather than be read as the default format.
|
|
@@ -137,50 +271,41 @@ module Grape
|
|
|
137
271
|
throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: 'The QUERY method requires a content-type.') if media_type.nil? && env[Rack::REQUEST_METHOD] == Grape::QUERY
|
|
138
272
|
|
|
139
273
|
fmt = media_type ? mime_types[media_type] : default_format
|
|
140
|
-
|
|
141
274
|
throw :error, Grape::Exceptions::ErrorResponse.new(status: 415, message: "The provided content-type '#{media_type}' is not supported.") unless content_type_for(fmt)
|
|
142
|
-
parser = Grape::Parser.parser_for fmt, parsers
|
|
143
|
-
return env[Grape::Env::API_REQUEST_BODY] = body unless parser
|
|
144
275
|
|
|
145
|
-
|
|
146
|
-
parsed = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
|
|
147
|
-
if parsed.is_a?(Hash)
|
|
148
|
-
if (form_hash = env[Rack::RACK_REQUEST_FORM_HASH])
|
|
149
|
-
form_hash.merge!(parsed)
|
|
150
|
-
else
|
|
151
|
-
env[Rack::RACK_REQUEST_FORM_HASH] = parsed
|
|
152
|
-
end
|
|
153
|
-
env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
|
|
154
|
-
end
|
|
155
|
-
rescue Grape::Exceptions::Base => e
|
|
156
|
-
raise e
|
|
157
|
-
rescue StandardError => e
|
|
158
|
-
throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
|
|
159
|
-
end
|
|
276
|
+
fmt
|
|
160
277
|
end
|
|
161
278
|
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
# -
|
|
165
|
-
#
|
|
166
|
-
# -
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
279
|
+
# What a parser raises that is not a Grape error, and so is answered as a
|
|
280
|
+
# 400 here. A Grape error goes on to the error middleware as it is. It
|
|
281
|
+
# used to be rescued just to be raised again, and re-raising at request
|
|
282
|
+
# depth cost about 25 µs -- paid by every malformed body, since the
|
|
283
|
+
# built-in parsers report one as InvalidMessageBody.
|
|
284
|
+
module ForeignParserError
|
|
285
|
+
def self.===(exception)
|
|
286
|
+
exception.is_a?(StandardError) && !exception.is_a?(Grape::Exceptions::Base)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
private_constant :ForeignParserError
|
|
290
|
+
|
|
291
|
+
# Whether the request carries a body at all, read off the env alone. Every
|
|
292
|
+
# request asks, and the ones answered no -- the GET, HEAD and OPTIONS
|
|
293
|
+
# majority, and a POST or DELETE with nothing in it -- are answered before
|
|
294
|
+
# CONTENT_TYPE is parsed or a Rack::Request is built. The env key is
|
|
295
|
+
# spelled out: on Rack 3, +Rack::CONTENT_LENGTH+ names the response header.
|
|
296
|
+
def body_given?(env)
|
|
173
297
|
return false unless BODY_CARRYING_METHODS.include?(env[Rack::REQUEST_METHOD])
|
|
174
|
-
return false if rack_request.form_data? && rack_request.content_type
|
|
175
|
-
return false if rack_request.parseable_data?
|
|
176
298
|
|
|
177
|
-
|
|
299
|
+
env['CONTENT_LENGTH'].to_i.positive? || env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
|
178
300
|
end
|
|
179
301
|
|
|
180
|
-
def negotiate_content_type
|
|
181
|
-
fmt = format_from_extension || format_from_query || format || format_from_header || default_format
|
|
302
|
+
def negotiate_content_type(env)
|
|
303
|
+
fmt = format_from_extension(env) || format_from_query(env) || format || format_from_header(env) || default_format
|
|
182
304
|
return env[Grape::Env::API_FORMAT] = fmt.to_sym if content_type_for(fmt)
|
|
183
305
|
|
|
306
|
+
synonym = declared_synonym(fmt)
|
|
307
|
+
return env[Grape::Env::API_FORMAT] = synonym if synonym
|
|
308
|
+
|
|
184
309
|
throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, message: "The requested format '#{fmt}' is not supported.")
|
|
185
310
|
end
|
|
186
311
|
|
|
@@ -189,13 +314,24 @@ module Grape
|
|
|
189
314
|
# sequence, and a +.+ byte cannot be part of a multi-byte one, so the dot
|
|
190
315
|
# sits at the same place before and after scrubbing. The overwhelming
|
|
191
316
|
# majority of paths carry no extension and now skip the scrub entirely.
|
|
192
|
-
def format_from_extension
|
|
193
|
-
request_path = path_for_extension
|
|
317
|
+
def format_from_extension(env)
|
|
318
|
+
request_path = path_for_extension(env)
|
|
194
319
|
dot_pos = request_path.rindex('.')
|
|
195
320
|
return unless dot_pos
|
|
196
321
|
|
|
197
|
-
|
|
198
|
-
|
|
322
|
+
# A start and a length rather than an endless Range, which is an object
|
|
323
|
+
# of its own built on every call. A length past the end is clamped.
|
|
324
|
+
extension = try_scrub(request_path[dot_pos + 1, request_path.length])
|
|
325
|
+
return extension if content_type_for(extension)
|
|
326
|
+
|
|
327
|
+
declared_synonym(extension)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# The name the API declared +fmt+ under when the request used another
|
|
331
|
+
# one for it (see Grape::ContentTypes::FORMAT_SYNONYMS), or nil.
|
|
332
|
+
def declared_synonym(fmt)
|
|
333
|
+
synonym = Grape::ContentTypes::FORMAT_SYNONYMS[fmt]
|
|
334
|
+
synonym if synonym && content_type_for(synonym)
|
|
199
335
|
end
|
|
200
336
|
|
|
201
337
|
# The extension is the tail of the request path, so PATH_INFO answers it
|
|
@@ -205,32 +341,37 @@ module Grape
|
|
|
205
341
|
# the String its concatenation allocates. Tested with +empty?+ rather
|
|
206
342
|
# than +blank?+: the path is not scrubbed yet, and a regexp match on an
|
|
207
343
|
# invalid byte sequence raises.
|
|
208
|
-
def path_for_extension
|
|
344
|
+
def path_for_extension(env)
|
|
209
345
|
path_info = env[Rack::PATH_INFO]
|
|
210
|
-
return
|
|
346
|
+
return Rack::Request.new(env).path if path_info.nil? || path_info.empty?
|
|
211
347
|
|
|
212
348
|
path_info
|
|
213
349
|
end
|
|
214
350
|
|
|
215
351
|
# +?format=+ can only be there when there is a query string at all, so
|
|
216
352
|
# the common query-less request skips parsing one.
|
|
217
|
-
|
|
353
|
+
#
|
|
354
|
+
# And a query string that spells neither +format+ nor a percent-escape
|
|
355
|
+
# cannot hold that key either: Rack splits the string on +&+ and +=+ and
|
|
356
|
+
# then unescapes each key, and unescaping only ever rewrites +%XX+ (or a
|
|
357
|
+
# +++, which becomes a space). So the parse -- the most expensive thing
|
|
358
|
+
# a request with a query string does, and one the endpoint may never ask
|
|
359
|
+
# for -- is left to whoever reads the params.
|
|
360
|
+
def format_from_query(env)
|
|
218
361
|
query_string = env[Rack::QUERY_STRING]
|
|
219
362
|
return if query_string.nil? || query_string.empty?
|
|
363
|
+
return unless query_string.include?(FORMAT_PARAM) || query_string.include?('%')
|
|
220
364
|
|
|
221
|
-
|
|
365
|
+
Rack::Request.new(env).GET[FORMAT_PARAM]
|
|
366
|
+
rescue *Grape::RACK_ERRORS
|
|
367
|
+
raise Grape::Exceptions::RequestError
|
|
222
368
|
end
|
|
223
369
|
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
accept_header = try_scrub(env['HTTP_ACCEPT'])
|
|
230
|
-
return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
|
|
231
|
-
|
|
232
|
-
media_type = Rack::Utils.best_q_match(accept_header.downcase, mime_types.keys)
|
|
233
|
-
mime_types[media_type] if media_type
|
|
370
|
+
# The keys are registered media types -- valid strings, which scrubbing
|
|
371
|
+
# leaves alone -- so only a miss needs the header scrubbed.
|
|
372
|
+
def format_from_header(env)
|
|
373
|
+
accept_header = env['HTTP_ACCEPT']
|
|
374
|
+
@format_for_accept.fetch(accept_header) { Formatter.format_for_accept(try_scrub(accept_header), mime_types) }
|
|
234
375
|
end
|
|
235
376
|
end
|
|
236
377
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Middleware
|
|
5
5
|
# Include in a middleware subclass that needs content-type negotiation.
|
|
6
6
|
# Provides +content_types+ / +mime_types+ / +content_type_for+ /
|
|
7
|
-
# +content_type+ resolved from +config.content_types+ and
|
|
7
|
+
# +media_type_for+ / +content_type+ resolved from +config.content_types+ and
|
|
8
8
|
# +config.format+ — so the consuming middleware's +Options+ Data class
|
|
9
9
|
# must declare both fields. Warms those caches on the parent instance
|
|
10
10
|
# at initialization so per-request +dup+s inherit them rather than
|
|
@@ -19,6 +19,9 @@ module Grape
|
|
|
19
19
|
# Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
|
|
20
20
|
# content-type-aware helpers don't pay for them.
|
|
21
21
|
module PrecomputedContentTypes
|
|
22
|
+
# Grape's own media type for each built-in format, under both spellings.
|
|
23
|
+
DEFAULT_CONTENT_TYPES = Grape::ContentTypes.lookup_for(Grape::ContentTypes::DEFAULTS)
|
|
24
|
+
|
|
22
25
|
def initialize(app, **options)
|
|
23
26
|
super
|
|
24
27
|
content_types
|
|
@@ -37,8 +40,17 @@ module Grape
|
|
|
37
40
|
content_types_lookup[format]
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
# The media type to label a response rendered in +format+ with: the one
|
|
44
|
+
# this API declares for it, or else Grape's own -- +api_format :txt+ in
|
|
45
|
+
# a +format :json+ API renders text/plain. #content_type_for stays nil
|
|
46
|
+
# for a format the API does not declare, since negotiation asks it
|
|
47
|
+
# whether the API supports the format at all.
|
|
48
|
+
def media_type_for(format)
|
|
49
|
+
content_type_for(format) || DEFAULT_CONTENT_TYPES[format]
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
def content_type
|
|
41
|
-
|
|
53
|
+
media_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
private
|
|
@@ -80,14 +80,13 @@ module Grape
|
|
|
80
80
|
middlewares.push(middleware)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Each spec ends in the block its operation was given, +nil+ when there
|
|
84
|
+
# was none. Telling the block apart by its class would take a Proc passed
|
|
85
|
+
# as the last argument, such as a callable the middleware is configured
|
|
86
|
+
# with, for the block.
|
|
83
87
|
def merge_with(middleware_specs)
|
|
84
|
-
middleware_specs.each do |operation,
|
|
85
|
-
|
|
86
|
-
last_proc = args.pop
|
|
87
|
-
public_send(operation, klass, *args, &last_proc)
|
|
88
|
-
else
|
|
89
|
-
public_send(operation, klass, *args)
|
|
90
|
-
end
|
|
88
|
+
middleware_specs.each do |operation, *args, block|
|
|
89
|
+
public_send(operation, *args, &block)
|
|
91
90
|
end
|
|
92
91
|
end
|
|
93
92
|
|
|
@@ -100,7 +99,7 @@ module Grape
|
|
|
100
99
|
end
|
|
101
100
|
|
|
102
101
|
# @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
|
|
103
|
-
# @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
|
|
102
|
+
# @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass, nil], [:insert_before, *args, block]])
|
|
104
103
|
def concat(other_specs)
|
|
105
104
|
use, not_use = other_specs.partition { |o| o.first == :use }
|
|
106
105
|
others << not_use
|
|
@@ -27,18 +27,30 @@ module Grape
|
|
|
27
27
|
|
|
28
28
|
attr_reader :available_media_types, :error_headers, :versions
|
|
29
29
|
|
|
30
|
+
# Read off ivars rather than delegated through +version_options+ into
|
|
31
|
+
# +config+: the versioners ask for +vendor+, +strict+ or +parameter+ on
|
|
32
|
+
# every request, and each read went two Forwardable frames and two Data
|
|
33
|
+
# readers deep for a value fixed when the middleware was built.
|
|
34
|
+
attr_reader :cascade, :parameter, :strict, :vendor
|
|
35
|
+
|
|
30
36
|
def_delegators :config, :mount_path, :prefix, :version_options
|
|
31
|
-
def_delegators :version_options, :cascade, :parameter, :strict, :vendor
|
|
32
37
|
|
|
33
38
|
def initialize(app, **options)
|
|
34
39
|
super
|
|
40
|
+
@cascade = version_options.cascade
|
|
41
|
+
@parameter = version_options.parameter
|
|
42
|
+
@strict = version_options.strict
|
|
43
|
+
# Media types are case-insensitive, and Grape::Util::MediaType parses
|
|
44
|
+
# an Accept header into lowercase; a vendor declared with capitals
|
|
45
|
+
# would otherwise never match one.
|
|
46
|
+
@vendor = version_options.vendor&.downcase
|
|
35
47
|
@versions = config.versions&.map(&:to_s) # making sure versions are strings to ease potential match
|
|
36
48
|
@error_headers = cascade ? CASCADE_PASS_HEADER : {}
|
|
37
49
|
@available_media_types = build_available_media_types
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
def potential_version_match?(potential_version)
|
|
41
|
-
versions.
|
|
53
|
+
versions.nil? || versions.empty? || versions.include?(potential_version)
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
def version_not_found!
|
|
@@ -50,15 +62,19 @@ module Grape
|
|
|
50
62
|
def build_available_media_types
|
|
51
63
|
media_types = []
|
|
52
64
|
base_media_type = "application/vnd.#{vendor}"
|
|
53
|
-
versions&.reverse_each
|
|
54
|
-
|
|
55
|
-
content_types.each_key { |extension| media_types << "#{versioned_base}+#{extension}" }
|
|
56
|
-
media_types << versioned_base
|
|
57
|
-
end
|
|
58
|
-
content_types.each_key { |extension| media_types << "#{base_media_type}+#{extension}" }
|
|
59
|
-
media_types << base_media_type
|
|
65
|
+
versions&.reverse_each { |version| append_media_types(media_types, "#{base_media_type}-#{version}") }
|
|
66
|
+
append_media_types(media_types, base_media_type)
|
|
60
67
|
media_types.concat(content_types.values.flatten)
|
|
61
|
-
|
|
68
|
+
# Media types are case-insensitive, and Grape::Util::MediaType matches
|
|
69
|
+
# an Accept header in lowercase; a version or content type declared
|
|
70
|
+
# with capitals would otherwise never match one.
|
|
71
|
+
media_types.map!(&:downcase)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# +base+ with each content type's extension, then +base+ alone.
|
|
75
|
+
def append_media_types(media_types, base)
|
|
76
|
+
content_types.each_key { |extension| media_types << "#{base}+#{extension}" }
|
|
77
|
+
media_types << base
|
|
62
78
|
end
|
|
63
79
|
end
|
|
64
80
|
end
|