Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#### Fixes

* [#2767](https://github.com/ruby-grape/grape/pull/2767): Update rubocop to 1.88.0 and rubocop-rspec to 3.10.2 - [@ericproulx](https://github.com/ericproulx).
* [#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).
* Your contribution here.

### 3.3.2 (2026-07-05)
Expand Down
27 changes: 10 additions & 17 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,19 @@ def add_head_not_allowed_methods_and_options_methods
# contain already versioning information when using path versioning.
all_routes = self.class.endpoints.flat_map(&:routes)

# Disable versioning so adding a route won't prepend versioning
# informations again.
without_root_prefix_and_versioning { collect_route_config_per_pattern(all_routes) }
# Read the settings the helper routes need from a *copy* with the
# root-prefix/versioning keys stripped, so adding these routes won't
# prepend versioning information again. This used to delete those keys
# from the shared class-level settings and restore them in an ensure;
# a request served concurrently on another instance during a runtime
# recompile could observe the missing keys (e.g. via #cascade?). A
# local copy keeps that mutation off the shared object.
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable.to_hash.except(*ROOT_PREFIX_VERSIONING_KEYS)
collect_route_config_per_pattern(all_routes, namespace_inheritable)
end

def collect_route_config_per_pattern(all_routes)
def collect_route_config_per_pattern(all_routes, namespace_inheritable)
routes_by_regexp = all_routes.group_by(&:pattern_regexp)
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable

# Build the configuration based on the first endpoint and the collection of methods supported.
routes_by_regexp.each_value do |routes|
Expand All @@ -177,18 +182,6 @@ def collect_route_config_per_pattern(all_routes)

ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze
private_constant :ROOT_PREFIX_VERSIONING_KEYS

# Allows definition of endpoints that ignore the versioning configuration
# used by the rest of your API.
def without_root_prefix_and_versioning
inheritable_setting = self.class.inheritable_setting
deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEYS)
yield
ensure
ROOT_PREFIX_VERSIONING_KEYS.zip(deleted_values) do |key, value|
inheritable_setting.namespace_inheritable[key] = value
end
end
end
end
end
21 changes: 21 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@

let(:app) { subject }

describe 'thread-safe (re)compilation' do
# Regression: building the HEAD/OPTIONS/405 helper routes used to delete the
# root-prefix/versioning keys from the shared class-level settings and
# restore them in an ensure. A request served concurrently on another
# instance during a runtime recompile could observe the missing keys (e.g.
# via #cascade?). Compilation must not mutate the shared settings.
it 'does not mutate the shared inheritable settings while (re)compiling' do
subject.version 'v1', using: :path, cascade: false
subject.get('/x') { 'x' }
subject.compile!

shared = subject.base_instance.inheritable_setting.namespace_inheritable
expect(shared).not_to receive(:delete)

subject.change!
subject.compile!

expect(shared[:version_options]).to be_present
end
end

describe '.prefix' do
it 'routes root through with the prefix' do
subject.prefix 'awesome/sauce'
Expand Down
Loading