Build helper routes from a settings copy, not shared mutation#2791
Open
ericproulx wants to merge 1 commit into
Open
Build helper routes from a settings copy, not shared mutation#2791ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
add_head_not_allowed_methods_and_options_methods used without_root_prefix_and_versioning to delete the root-prefix/versioning keys from the shared class-level namespace_inheritable and restore them in an ensure. Compilation runs when a fresh API instance is built, which can happen at runtime (mount, helpers, or route definitions call change!) while another instance is still serving. A request handled during that window could observe the temporarily-missing keys — e.g. Instance#cascade? reads namespace_inheritable[:version_options] and would fall back to true. Read the settings the helper routes need from a local copy (namespace_inheritable.to_hash minus the versioning keys) and pass it into collect_route_config_per_pattern instead. Nothing in that path reads the stripped keys, so behavior is unchanged, and the shared settings object is no longer mutated during compilation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1a14108 to
847a019
Compare
Danger ReportNo issues found. |
Danger ReportWarnings
MarkdownsHere's an example of a CHANGELOG.md entry: * [#2791](https://github.com/ruby-grape/grape/pull/2791): Build helper routes from a settings copy, not shared mutation - [@ericproulx](https://github.com/ericproulx). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2790, from the same thread-safety review.
Problem
When a
Grape::APIinstance is built,add_head_not_allowed_methods_and_options_methodswraps its work inwithout_root_prefix_and_versioning, which deletes the root-prefix/versioning keys from the shared class-levelnamespace_inheritableand restores them in anensure:Instance construction happens whenever the API is (re)compiled, and that can occur at runtime —
mount,helpers, and route definitions all callchange!, which nils@instanceso the next request rebuilds it. If another instance is serving a request during the delete→restore window, it can observe the missing keys. Concretely,Instance#cascade?readsnamespace_inheritable[:version_options]and would fall back totrue, so a response'sX-Cascadeheader could be stripped (or not) incorrectly for that request.This is narrow (only affects apps that mutate their API definition at runtime while serving) but it's a real shared-state mutation on a settings object that live requests read.
Fix
Nothing in
collect_route_config_per_patternactually reads the stripped keys — it only reads:do_not_route_head/:do_not_route_options, and the routes/paths are already built before the block. So instead of mutating the shared object, read what the helper routes need from a local copy with the versioning keys removed:without_root_prefix_and_versioningis removed; the shared settings are never mutated during compilation. Behavior is unchanged.Tests
New regression in
spec/grape/api_spec.rbasserts that (re)compiling a versioned API does notdeletefrom the sharednamespace_inheritableand that:version_optionsstays present. Verified it fails against the old delete/restore implementation. Full suite: 2352 examples, 0 failures. RuboCop clean.🤖 Generated with Claude Code