Catch exceptions raised inside rescue_from blocks (#2482)#2703
Merged
Conversation
8fa211c to
95b26c7
Compare
3 tasks
95b26c7 to
eebd072
Compare
Danger ReportNo issues found. |
eebd072 to
8101553
Compare
Resolves #2482. An exception raised inside a `rescue_from` block used to bubble uncaught and produce the Rack default 500 page. The middleware now catches it and routes it through one of three paths: 1. The exception's class has a registered `rescue_from` handler → run that handler (with `redispatched: true` so a second raise stops the chain — bounded at one redispatch). 2. The exception is a `Grape::Exceptions::Base` subclass → render it via the default Grape error path with its own status and message. 3. Otherwise → fall through to safe_default. The original is exposed on `env['grape.exception']` for upstream Rack middleware to observe. If the user opted into `rescue_from :internal_grape_exceptions do |e| ... end`, that handler runs and owns the response. Otherwise the framework renders `Grape::Exceptions::InternalServerError` (status 500, generic message) — never the original exception's message. The framework deliberately does no logging of its own for unhandled internal exceptions. Formatting and destination are application concerns; the `:internal_grape_exceptions` handler is where the application opts in to logging, error-tracker forwarding, or any other observability it cares about. Adds: * `Grape::Exceptions::InternalServerError` — i18n-backed safe stand-in. * `Grape::Env::GRAPE_EXCEPTION` — env key carrying the original. * `rescue_from :internal_grape_exceptions` — DSL opt-in mirroring `:grape_exceptions`. Lets the application capture the exception and shape the response. Existing behaviour preserved when the handler returns a non-Response non-error value (still produces `InvalidResponse` via the default handler). UPGRADING note + README rescue_from section document the new behaviour and the `:internal_grape_exceptions` opt-in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8101553 to
006cc72
Compare
dblock
approved these changes
May 10, 2026
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.
Resolves #2482. Catches exceptions raised inside
rescue_fromblocks and routes them through one of three paths instead of letting them bubble up to Rack as a default 500 page.Behavior
rescue_fromhandlerGrape::Exceptions::Basesubclass with no registered handlerstatusandmessageStandardError,NoMethodError, etc.)env['grape.exception']. If the user opted intorescue_from :internal_grape_exceptions, their handler runs and owns the response. Otherwise framework renders a genericGrape::Exceptions::InternalServerError(500). The original exception's message is never surfaced to the API consumer in this branch.The third bucket is the safety property the issue thread was missing — neither the workaround in the issue body nor PR #2483 have it. A typo in a
rescue_fromblock (e.g.rescue_from :all do; foo; endwherefoois undefined) used to bubble up as a Rack 500 and now becomes a Grape500 Internal Server Errorwith the actualNoMethodErrorobservable inenv['grape.exception'], but never rendered.New
rescue_from :internal_grape_exceptionsopt-inMirrors the existing
rescue_from :grape_exceptionskeyword. When registered, the handler is invoked for any unrecognised internal error — typically what you want for forwarding to error trackers, custom logging, request-id correlation, etc.:The framework hands you the original exception and steps out of the way. The framework does no logging of its own — formatting and destination are application concerns;
:internal_grape_exceptionsis where you opt in to whatever observability you want.Why this resolves the issue
@ngernert's actual code in #2482 was:
After this PR, that simplifies to:
The first handler re-raises; the second handler is invoked through the redispatch.
How this differs from PR #2483
PR #2483 catches
StandardErrorand forwards the caught exception todefault_rescue_handler, which rendersexception.messageas the response body. That fixes the issue author's specific use case but:NoMethodError) ends up rendered as"undefined method 'foo' for nil:NilClass"to the API consumer.rescue_fromhandler is sent to the default handler, not the user's handler.handler_exception || InvalidResponsefallback.This PR addresses all of that: redispatch through the user's chain when applicable, render only Grape-aware exceptions with their own message, expose unrecognised exceptions in env, and let the application opt in to owning the unrecognised-error path entirely.
Files
lib/grape/middleware/error.rbrun_rescue_handlergainsredispatched:kwarg; new privateredispatch,safe_default,framework_default. Newinternal_grape_exceptions_rescue_handleroption.lib/grape/dsl/request_response.rbrescue_from :internal_grape_exceptionsrecognized as a keyword and stored innamespace_inheritable[:internal_grape_exceptions_rescue_handler].lib/grape/endpoint.rberror_middleware_optionshelper since the kwarg list grew.lib/grape/exceptions/internal_server_error.rbGrape::Exceptions::InternalServerError < Grape::Exceptions::Base— status 500, i18n-backed generic message.lib/grape/env.rbGrape::Env::GRAPE_EXCEPTION = 'grape.exception'— exposed for upstream Rack middleware (also relevant to #2610).lib/grape/locale/en.ymlinternal_server_error: 'Internal Server Error'i18n entry.spec/grape/middleware/error_spec.rbenv['grape.exception']exposure.README.mdrescue_fromsection documenting re-raise semantics and the opt-in.UPGRADING.mdTest plan
bundle exec rspec spec/grape/middleware/error_spec.rb— 16 examples, 0 failures (9 new + 7 existing)bundle exec rspec— full suite green (2,263 examples, up from 2,254)bundle exec rubocop lib/ spec/— cleanNote on observability
Stashing the original exception on
env['grape.exception']overlaps with the request in #2610 (Rails-styleaction_dispatch.exceptionequivalent). It's a free side-effect of the safe-default path here. If maintainers want full #2610 coverage — exposing the original exception in env for every swallowed exception, not just re-raises fromrescue_fromblocks — that's a separate small change inerror_response/call!.🤖 Generated with Claude Code