grape 3.3.2 → 3.3.3
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 +9 -1
- data/README.md +1 -1
- data/lib/grape/api/instance.rb +12 -17
- data/lib/grape/api.rb +3 -0
- data/lib/grape/content_types.rb +13 -1
- data/lib/grape/dsl/routing.rb +3 -1
- data/lib/grape/endpoint.rb +32 -7
- data/lib/grape/middleware/error.rb +12 -1
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/router.rb +10 -5
- data/lib/grape/version.rb +1 -2
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d535091a3993a05695219506182f9259f499ee04379dfc023467c22fb56c47b
|
|
4
|
+
data.tar.gz: 2f2d84b73bed8ce3425e601f4619ea53b0287a6a466ef8b2a7c2fcb501bb980a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0c882bfce6eef4f8ff9942a60dd06841cea7e01cf93d2b9af1427b766ec86636f14f31941a04c7ea844ff08cef2e4f720bd2d45bf6f61c4d8ccd983ea55bde8
|
|
7
|
+
data.tar.gz: 8c974d32f17599683db1ca2902c82c793013c87628f3fcd16be105d5c930109a9834fe7445411aec06e94c2e5635bd90441bc93081ceaca11c256c7f4e4f3cbd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
### 3.3.3 (2026-07-17)
|
|
2
|
+
|
|
3
|
+
#### Fixes
|
|
4
|
+
|
|
5
|
+
* [#2794](https://github.com/ruby-grape/grape/pull/2794): Fix `rescue_from Klass, rescue_subclasses: false` never rescuing — base-only handlers were written to and read from different settings stores - [@ericproulx](https://github.com/ericproulx).
|
|
6
|
+
* [#2789](https://github.com/ruby-grape/grape/pull/2789): Escape error messages served with a parameterized `text/html` content-type (e.g. `text/html; charset=utf-8`) and warn when a bare Rack app is mounted under authentication - [@ericproulx](https://github.com/ericproulx).
|
|
7
|
+
* [#2790](https://github.com/ruby-grape/grape/pull/2790): Stop mutating `Grape::Router`'s internal maps at request time for HTTP methods with no routes (data race on JRuby/TruffleRuby and unbounded growth) - [@ericproulx](https://github.com/ericproulx).
|
|
8
|
+
* [#2791](https://github.com/ruby-grape/grape/pull/2791): Build HEAD/OPTIONS/405 helper routes from a copy of the inheritable settings instead of temporarily mutating the shared class-level settings during compilation - [@ericproulx](https://github.com/ericproulx).
|
|
9
|
+
|
|
1
10
|
### 3.3.2 (2026-07-05)
|
|
2
11
|
|
|
3
12
|
#### Fixes
|
|
4
13
|
|
|
5
14
|
* [#2773](https://github.com/ruby-grape/grape/pull/2773): Fix middleware build crash when a top-level `::Options` constant is in scope - [@ericproulx](https://github.com/ericproulx).
|
|
6
|
-
* Your contribution here.
|
|
7
15
|
|
|
8
16
|
### 3.3.1 (2026-06-28)
|
|
9
17
|
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com
|
|
|
10
10
|
|
|
11
11
|
## Stable Release
|
|
12
12
|
|
|
13
|
-
You're reading the documentation for the stable release of Grape, 3.3.
|
|
13
|
+
You're reading the documentation for the stable release of Grape, 3.3.3.
|
|
14
14
|
|
|
15
15
|
## Project Resources
|
|
16
16
|
|
data/lib/grape/api/instance.rb
CHANGED
|
@@ -5,6 +5,8 @@ module Grape
|
|
|
5
5
|
# The API Instance class, is the engine behind Grape::API. Each class that inherits
|
|
6
6
|
# from this will represent a different API instance
|
|
7
7
|
class Instance
|
|
8
|
+
# Marks this and every mounted instance as a mountable Grape app (see Grape::Mountable).
|
|
9
|
+
extend Grape::Mountable
|
|
8
10
|
extend Grape::DSL::Settings
|
|
9
11
|
extend Grape::DSL::Desc
|
|
10
12
|
extend Grape::DSL::Validations
|
|
@@ -150,14 +152,19 @@ module Grape
|
|
|
150
152
|
# contain already versioning information when using path versioning.
|
|
151
153
|
all_routes = self.class.endpoints.flat_map(&:routes)
|
|
152
154
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
|
|
155
|
+
# Read the settings the helper routes need from a *copy* with the
|
|
156
|
+
# root-prefix/versioning keys stripped, so adding these routes won't
|
|
157
|
+
# prepend versioning information again. This used to delete those keys
|
|
158
|
+
# from the shared class-level settings and restore them in an ensure;
|
|
159
|
+
# a request served concurrently on another instance during a runtime
|
|
160
|
+
# recompile could observe the missing keys (e.g. via #cascade?). A
|
|
161
|
+
# local copy keeps that mutation off the shared object.
|
|
162
|
+
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable.to_hash.except(*ROOT_PREFIX_VERSIONING_KEYS)
|
|
163
|
+
collect_route_config_per_pattern(all_routes, namespace_inheritable)
|
|
156
164
|
end
|
|
157
165
|
|
|
158
|
-
def collect_route_config_per_pattern(all_routes)
|
|
166
|
+
def collect_route_config_per_pattern(all_routes, namespace_inheritable)
|
|
159
167
|
routes_by_regexp = all_routes.group_by(&:pattern_regexp)
|
|
160
|
-
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
|
|
161
168
|
|
|
162
169
|
# Build the configuration based on the first endpoint and the collection of methods supported.
|
|
163
170
|
routes_by_regexp.each_value do |routes|
|
|
@@ -177,18 +184,6 @@ module Grape
|
|
|
177
184
|
|
|
178
185
|
ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze
|
|
179
186
|
private_constant :ROOT_PREFIX_VERSIONING_KEYS
|
|
180
|
-
|
|
181
|
-
# Allows definition of endpoints that ignore the versioning configuration
|
|
182
|
-
# used by the rest of your API.
|
|
183
|
-
def without_root_prefix_and_versioning
|
|
184
|
-
inheritable_setting = self.class.inheritable_setting
|
|
185
|
-
deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEYS)
|
|
186
|
-
yield
|
|
187
|
-
ensure
|
|
188
|
-
ROOT_PREFIX_VERSIONING_KEYS.zip(deleted_values) do |key, value|
|
|
189
|
-
inheritable_setting.namespace_inheritable[key] = value
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
187
|
end
|
|
193
188
|
end
|
|
194
189
|
end
|
data/lib/grape/api.rb
CHANGED
|
@@ -4,6 +4,9 @@ module Grape
|
|
|
4
4
|
# The API class is the primary entry point for creating Grape APIs. Users
|
|
5
5
|
# should subclass this class in order to build an API.
|
|
6
6
|
class API
|
|
7
|
+
# Marks this and every subclass as a mountable Grape app (see Grape::Mountable).
|
|
8
|
+
extend Grape::Mountable
|
|
9
|
+
|
|
7
10
|
# Class methods that we want to call on the API rather than on the API object
|
|
8
11
|
NON_OVERRIDABLE = %i[base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting= top_level_setting].freeze
|
|
9
12
|
|
data/lib/grape/content_types.rb
CHANGED
|
@@ -22,7 +22,19 @@ module Grape
|
|
|
22
22
|
def mime_types_for(from_settings)
|
|
23
23
|
return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS
|
|
24
24
|
|
|
25
|
-
from_settings.invert.transform_keys! { |k|
|
|
25
|
+
from_settings.invert.transform_keys! { |k| media_type(k) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The media type of a content-type header: the part before any `;`
|
|
29
|
+
# parameters, with surrounding whitespace removed
|
|
30
|
+
# (e.g. `'text/html'` for `'text/html; charset=utf-8'`). Returns nil for a
|
|
31
|
+
# nil content type. Skips the split (and its allocation) when there are no
|
|
32
|
+
# parameters, which is the common case.
|
|
33
|
+
def media_type(content_type)
|
|
34
|
+
return if content_type.nil?
|
|
35
|
+
|
|
36
|
+
base = content_type.include?(';') ? content_type.split(';', 2).first : content_type
|
|
37
|
+
base.strip
|
|
26
38
|
end
|
|
27
39
|
end
|
|
28
40
|
end
|
data/lib/grape/dsl/routing.rb
CHANGED
|
@@ -133,7 +133,9 @@ module Grape
|
|
|
133
133
|
end
|
|
134
134
|
in_setting = inheritable_setting
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
# Past the mount_instance branch above, a Grape app here is an already
|
|
137
|
+
# instantiated Grape::API::Instance (vs. a bare Rack app).
|
|
138
|
+
if app.is_a?(Grape::Mountable)
|
|
137
139
|
mount_path = Grape::Util::PathNormalizer.call(path)
|
|
138
140
|
app.top_level_setting.namespace_stackable[:mount_path] = mount_path
|
|
139
141
|
|
data/lib/grape/endpoint.rb
CHANGED
|
@@ -261,6 +261,7 @@ module Grape
|
|
|
261
261
|
|
|
262
262
|
def compile!
|
|
263
263
|
@app = config.app || build_stack
|
|
264
|
+
warn_unauthenticated_mounted_app
|
|
264
265
|
@helpers = build_helpers
|
|
265
266
|
stackable = inheritable_setting.namespace_stackable
|
|
266
267
|
@befores = stackable[:befores]
|
|
@@ -294,6 +295,13 @@ module Grape
|
|
|
294
295
|
end
|
|
295
296
|
end
|
|
296
297
|
|
|
298
|
+
# True when a bare Rack app (anything that isn't a Grape app) is mounted at
|
|
299
|
+
# this endpoint. Such an app is called directly and matched by path prefix
|
|
300
|
+
# rather than an anchored route.
|
|
301
|
+
def bare_rack_app?
|
|
302
|
+
config.app && !config.app.is_a?(Grape::Mountable)
|
|
303
|
+
end
|
|
304
|
+
|
|
297
305
|
def prepare_default_route_attributes(route_options)
|
|
298
306
|
{
|
|
299
307
|
namespace:,
|
|
@@ -369,8 +377,8 @@ module Grape
|
|
|
369
377
|
default_error_formatter: ns_inh[:default_error_formatter],
|
|
370
378
|
error_formatters: ns_stack.namespace_stackable_with_hash(:error_formatters),
|
|
371
379
|
rescue_options: ns_stack.namespace_stackable[:rescue_options]&.last,
|
|
372
|
-
rescue_handlers
|
|
373
|
-
base_only_rescue_handlers:
|
|
380
|
+
rescue_handlers: merged_reverse_stackable(:rescue_handlers),
|
|
381
|
+
base_only_rescue_handlers: merged_reverse_stackable(:base_only_rescue_handlers),
|
|
374
382
|
all_rescue_handler: ns_inh[:all_rescue_handler],
|
|
375
383
|
grape_exceptions_rescue_handler: ns_inh[:grape_exceptions_rescue_handler],
|
|
376
384
|
internal_grape_exceptions_rescue_handler: ns_inh[:internal_grape_exceptions_rescue_handler]
|
|
@@ -384,6 +392,19 @@ module Grape
|
|
|
384
392
|
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
|
|
385
393
|
end
|
|
386
394
|
|
|
395
|
+
# A bare Rack app mounted with +mount+ is called directly (see +compile!+):
|
|
396
|
+
# it does not go through +build_stack+, so the API's authentication
|
|
397
|
+
# middleware never runs and the mount is reachable unauthenticated. Mounted
|
|
398
|
+
# Grape APIs are unaffected because they rebuild their own stack from the
|
|
399
|
+
# inherited settings. Warn so this bypass isn't silent.
|
|
400
|
+
def warn_unauthenticated_mounted_app
|
|
401
|
+
return unless bare_rack_app?
|
|
402
|
+
return unless inheritable_setting.namespace_inheritable[:auth]
|
|
403
|
+
|
|
404
|
+
warn "Grape: #{config.app} is mounted under an API that declares authentication, but authentication " \
|
|
405
|
+
'middleware does not wrap mounted Rack applications. Requests to this mount are not authenticated by Grape.'
|
|
406
|
+
end
|
|
407
|
+
|
|
387
408
|
def build_response_cookies
|
|
388
409
|
return unless request.cookies?
|
|
389
410
|
|
|
@@ -397,12 +418,16 @@ module Grape
|
|
|
397
418
|
inheritable_setting.namespace_inheritable[:lint] || Grape.config.lint
|
|
398
419
|
end
|
|
399
420
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
# Merge a reverse-stackable handler map (as written by +rescue_from+) into a
|
|
422
|
+
# single Hash. The reverse store lists child-scope handlers before inherited
|
|
423
|
+
# ones, and the first-wins merge keeps the child's handler for a given
|
|
424
|
+
# class, so a nested +rescue_from+ overrides an outer one.
|
|
425
|
+
def merged_reverse_stackable(key)
|
|
426
|
+
handlers = inheritable_setting.namespace_reverse_stackable[key]
|
|
427
|
+
return if handlers.blank?
|
|
403
428
|
|
|
404
|
-
|
|
405
|
-
result.merge!(
|
|
429
|
+
handlers.each_with_object({}) do |handler, result|
|
|
430
|
+
result.merge!(handler) { |_k, s1, _s2| s1 }
|
|
406
431
|
end
|
|
407
432
|
end
|
|
408
433
|
end
|
|
@@ -58,10 +58,21 @@ module Grape
|
|
|
58
58
|
private
|
|
59
59
|
|
|
60
60
|
def rack_response(status, headers, message)
|
|
61
|
-
message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE]
|
|
61
|
+
message = Rack::Utils.escape_html(message) if html_content_type?(headers[Rack::CONTENT_TYPE])
|
|
62
62
|
Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Escaping must key off the media type only, case-insensitively. Comparing
|
|
66
|
+
# the raw header against 'text/html' would let a parameterized value such
|
|
67
|
+
# as 'text/html; charset=utf-8' (or a differently-cased 'Text/HTML', which
|
|
68
|
+
# browsers still treat as HTML) skip escaping and reflect an unescaped
|
|
69
|
+
# message into an HTML response. Such a header can be set from several
|
|
70
|
+
# places (a registered content type, or a custom Content-Type passed to
|
|
71
|
+
# error!/rescue_from), but they all render here.
|
|
72
|
+
def html_content_type?(content_type)
|
|
73
|
+
Grape::ContentTypes.media_type(content_type).to_s.casecmp?('text/html')
|
|
74
|
+
end
|
|
75
|
+
|
|
65
76
|
def format_message(error)
|
|
66
77
|
current_format = env[Grape::Env::API_FORMAT] || format
|
|
67
78
|
formatter = Grape::ErrorFormatter.formatter_for(current_format, error_formatters, default_error_formatter)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
# Marker module for a mountable Grape application. Both {Grape::API} (the
|
|
5
|
+
# remountable user-facing class) and {Grape::API::Instance} (the compiled
|
|
6
|
+
# engine) extend it, so a mounted Grape app can be told apart from a bare
|
|
7
|
+
# Rack app with `is_a?(Grape::Mountable)` — a single, explicit predicate
|
|
8
|
+
# rather than duck-typing on an incidental internal method such as
|
|
9
|
+
# `inheritable_setting`.
|
|
10
|
+
#
|
|
11
|
+
# `Grape::API` and `Grape::API::Instance` are not related by inheritance and
|
|
12
|
+
# do not even respond to the same methods (the former to `mount_instance`,
|
|
13
|
+
# the latter to `inheritable_setting`/`endpoints`), so there is no common
|
|
14
|
+
# ancestor to key an `is_a?` check on without this marker.
|
|
15
|
+
#
|
|
16
|
+
# It answers identity only ("is this a Grape app?"). Capability checks that
|
|
17
|
+
# go on to call a stage-specific method — e.g. `respond_to?(:endpoints)`
|
|
18
|
+
# before reading `endpoints` — must stay as they are, since a `Mountable`
|
|
19
|
+
# does not necessarily respond to every such method.
|
|
20
|
+
module Mountable
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/grape/router.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Grape
|
|
|
13
13
|
def initialize
|
|
14
14
|
@neutral_map = []
|
|
15
15
|
@neutral_regexes = []
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# Plain hashes with no auto-vivifying default: a lookup for an HTTP method
|
|
17
|
+
# that has no routes must not insert a key. `compile!` freezes both maps,
|
|
18
|
+
# so request-time reads never mutate shared state (see #match? / #rotation).
|
|
19
|
+
@map = {}
|
|
20
|
+
@optimized_map = {}
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def compile!
|
|
@@ -29,11 +32,13 @@ module Grape
|
|
|
29
32
|
optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
|
|
30
33
|
@optimized_map[method] = Regexp.union(optimized_map)
|
|
31
34
|
end
|
|
35
|
+
@map.freeze
|
|
36
|
+
@optimized_map.freeze
|
|
32
37
|
@compiled = true
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def append(route)
|
|
36
|
-
@map[route.request_method] << route
|
|
41
|
+
(@map[route.request_method] ||= []) << route
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def associate_routes(greedy_route)
|
|
@@ -73,7 +78,7 @@ module Grape
|
|
|
73
78
|
|
|
74
79
|
def rotation(input, method, env, exact_route)
|
|
75
80
|
response = nil
|
|
76
|
-
@map[method]
|
|
81
|
+
@map[method]&.each do |route|
|
|
77
82
|
next if exact_route == route
|
|
78
83
|
next unless route.match?(input)
|
|
79
84
|
|
|
@@ -139,7 +144,7 @@ module Grape
|
|
|
139
144
|
end
|
|
140
145
|
|
|
141
146
|
def match?(input, method)
|
|
142
|
-
@optimized_map[method]
|
|
147
|
+
@optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } }
|
|
143
148
|
end
|
|
144
149
|
|
|
145
150
|
def greedy_match?(input)
|
data/lib/grape/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grape
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.3.
|
|
4
|
+
version: 3.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Bleigh
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/grape/middleware/versioner/header.rb
|
|
189
189
|
- lib/grape/middleware/versioner/param.rb
|
|
190
190
|
- lib/grape/middleware/versioner/path.rb
|
|
191
|
+
- lib/grape/mountable.rb
|
|
191
192
|
- lib/grape/namespace.rb
|
|
192
193
|
- lib/grape/params_builder.rb
|
|
193
194
|
- lib/grape/params_builder/base.rb
|
|
@@ -281,9 +282,9 @@ licenses:
|
|
|
281
282
|
- MIT
|
|
282
283
|
metadata:
|
|
283
284
|
bug_tracker_uri: https://github.com/ruby-grape/grape/issues
|
|
284
|
-
changelog_uri: https://github.com/ruby-grape/grape/blob/v3.3.
|
|
285
|
-
documentation_uri: https://www.rubydoc.info/gems/grape/3.3.
|
|
286
|
-
source_code_uri: https://github.com/ruby-grape/grape/tree/v3.3.
|
|
285
|
+
changelog_uri: https://github.com/ruby-grape/grape/blob/v3.3.3/CHANGELOG.md
|
|
286
|
+
documentation_uri: https://www.rubydoc.info/gems/grape/3.3.3
|
|
287
|
+
source_code_uri: https://github.com/ruby-grape/grape/tree/v3.3.3
|
|
287
288
|
rubygems_mfa_required: 'true'
|
|
288
289
|
rdoc_options: []
|
|
289
290
|
require_paths:
|