Give point-in-time copies their own stackable and rescue-handler stores - #2841
Open
ericproulx wants to merge 1 commit into
Open
Give point-in-time copies their own stackable and rescue-handler stores#2841ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
ericproulx
force-pushed
the
fix/stackable-copy-aliasing
branch
from
August 1, 2026 09:04
0ef3dc2 to
eb7cf0d
Compare
Danger ReportNo issues found. |
InheritableSetting#point_in_time_copy copied a scope's stackable store and
its rescue-handler maps shallowly, so the nested Arrays and Hashes stayed
shared with the source. A registration made after an endpoint was defined
therefore still reached that endpoint -- but only when the key already held a
registration at the time the copy was taken, since otherwise the writer
allocated a fresh store on the source alone.
That made the outcome depend on something the API never expressed:
use Middleware1
get('/x') { }
use Middleware2 # applied to GET /x
get('/x') { }
use Middleware2 # did NOT apply to GET /x
and the same for rescue_from:
rescue_from ArgumentError { }
get('/x') { raise Boom }
rescue_from Boom { } # rescued GET /x
get('/x') { raise Boom }
rescue_from Boom { } # did NOT rescue GET /x
Two APIs stating the same thing, behaving differently. Not a deliberate
"late registration" feature -- helpers defined after an endpoint already did
not leak, because they resolve down a different path.
Dup the nested stores as well as the Hash holding them. A copy is a point in
time: what the source registers afterwards must not reach it. Inheritance is
untouched -- it resolves by walking #parent, so a scope still sees values an
enclosing scope gains later, which is what the existing "decouples namespace
stackable values" spec actually exercised (its value lives on the parent, so
nothing was ever shared and it passed either way).
Boot cost is negligible: 300 endpoints under scopes carrying middleware,
helpers and filters allocate 1800 more objects (+0.4%) with no measurable
change in time, all at definition time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
fix/stackable-copy-aliasing
branch
from
August 1, 2026 09:56
eb7cf0d to
2599501
Compare
4 tasks
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.
Summary
InheritableSetting#point_in_time_copycopied a scope's stackable store and its rescue-handler maps shallowly, so the nested Arrays and Hashes stayed shared with the source. A registration made after an endpoint was defined therefore still reached that endpoint — but only when the key already held a registration at the time the copy was taken, since otherwise the writer allocated a fresh store on the source alone.The outcome depended on something the API never expressed:
use MW1· route ·use MW2use MW2rescue_from A· route ·rescue_from Brescue_from BTwo APIs stating the same thing, behaving differently. Not a deliberate "late registration" feature either — helpers defined after an endpoint already did not leak, because they resolve down a different path.
Approach
Dup the nested stores as well as the Hash holding them. A copy is a point in time: what the source registers afterwards must not reach it.
Inheritance is untouched — it resolves by walking
#parent, so a scope still sees values an enclosing scope gains later. That is what the existingdecouples namespace stackable valuesspec actually exercised: its value lives on the parent, so nothing was ever shared and it passed with or without this change. The new specs register on the scope itself before taking the copy, which is the shape that leaked.Backward compatibility
UPGRADING entry added. An API that declares
use/helpers/before/rescue_frombelow its routes and relies on it applying to them will now see it silently not run. That arrangement only ever worked when an earlier registration for the same key happened to seed the store, so it was never dependable; the fix is to move the registration above the routes it should cover, which is where the documentation has always placed it.Perf
Definition-time only. 300 endpoints under scopes carrying 8 middleware, 4 helpers and 6 filters:
Test plan
inheritable_setting_spec.rb(stackable and rescue-handler leakage in both directions, sibling independence, base-only handlers) and 4 integration examples inapi_spec.rbcoveringuseandrescue_fromin both the seeded and unseeded arrangements; verified the behavioural ones fail without thelib/change.🤖 Generated with Claude Code