Skip to content

Fix base-only rescue handlers never firing#2794

Merged
ericproulx merged 6 commits into
masterfrom
fix/base-only-rescue-handlers
Jul 13, 2026
Merged

Fix base-only rescue handlers never firing#2794
ericproulx merged 6 commits into
masterfrom
fix/base-only-rescue-handlers

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Bug

rescue_from SomeError, rescue_subclasses: false never rescues anything — the exception propagates uncaught:

class E1 < StandardError; end
rescue_from E1, rescue_subclasses: false do
  error!('handled', 418)
end
get('/x') { raise E1 }
# GET /x → E1 raised, uncaught (should be 418)

Cause

The write and read use different settings stores:

  • Write (dsl/request_response.rb): namespace_reverse_stackable[:base_only_rescue_handlers] = …
  • Read (endpoint.rb#error_middleware_options): namespace_stackable_with_hash(:base_only_rescue_handlers) — reads namespace_stackable

namespace_stackable[:base_only_rescue_handlers] is never written, so the middleware's base_only_rescue_handlers is always empty and the klass == err branch in registered_rescue_handler finds nothing.

:rescue_handlers (the rescue_subclasses: true default) is unaffected — it's both written and read via namespace_reverse_stackable.

Likely introduced by 60c1a99e, which moved the base-only read to namespace_stackable_with_hash while the write stayed on namespace_reverse_stackable.

Fix

Read base-only handlers from the same reverse-stackable store the write uses, sharing the child-precedence merge with rescue_handlers (merged_reverse_stackable). Base-only handlers now also get the nested-scope precedence subclass handlers already had.

Why it slipped through

The only behavioral test asserted a child error is not rescued — which passes even when base-only is 100% broken (a broken handler catches nothing). Nothing asserted the exact class is rescued. Added that test; verified it fails on the old code and passes on the fix.

Full suite: 2352 examples, 0 failures. RuboCop clean.

🤖 Generated with Claude Code

`rescue_from Klass, rescue_subclasses: false` registered its handler in
`namespace_reverse_stackable[:base_only_rescue_handlers]` (dsl/request_response.rb),
but the error middleware options read `:base_only_rescue_handlers` from
`namespace_stackable` via `namespace_stackable_with_hash`. Those are two
different stores, so the read was always empty and base-only handlers never
ran — the exception propagated uncaught.

Read base-only handlers from the same reverse-stackable store the write uses,
sharing the child-precedence merge with `rescue_handlers` via
`merged_reverse_stackable`. This also gives base-only handlers the same
nested-scope precedence that subclass handlers already had.

The gap slipped through because the only behavioral test asserted a *child*
error is not rescued (which passes even when base-only is fully broken);
nothing asserted the exact class is rescued. Add that missing test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the fix/base-only-rescue-handlers branch from 7dd1b39 to 60dcb27 Compare July 12, 2026 13:18
@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

namespace_reverse_stackable existed only to store rescue-handler maps, yet it
was a public attr_reader that callers reached into with raw keys from two
different files (the rescue_from write and the endpoint read). That's how the
base-only handlers write/read drifted onto mismatched stores.

Make the store internal (protected, still used by inherit_from/copy_state_from/
to_hash) and expose intent-revealing methods on InheritableSetting instead:
rescue_handlers / base_only_rescue_handlers (merged, child-scope-first) and
add_rescue_handlers(mapping, subclasses:). The DSL write and endpoint reads go
through those, so there's one place that knows the key, store and merge.

The reverse-ordering behaviour stays covered by ReverseStackableValues' own
spec; the InheritableSetting specs that poked the raw store are replaced with
tests for the new methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ericproulx ericproulx merged commit 1d94591 into master Jul 13, 2026
69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants