Skip to content

Let rescue_from :grape_exceptions outrank a catch-all class handler - #2843

Open
ericproulx wants to merge 1 commit into
masterfrom
fix/grape-exceptions-precedence
Open

Let rescue_from :grape_exceptions outrank a catch-all class handler#2843
ericproulx wants to merge 1 commit into
masterfrom
fix/grape-exceptions-precedence

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

rescue_from :grape_exceptions is an opt-in to keep Grape's own errors rendering with their own status — a validation failure answers 400 rather than whatever the application's catch-all returns.

It only ever worked against rescue_from :all, which lives in all_rescue_handler and is consulted last. Written as a class instead, rescue_from StandardError is a registered handler, matched first, and Grape's exceptions are StandardErrors — so the opt-in silently did nothing:

rescue_from StandardError { error!('server error', 500) }
rescue_from :grape_exceptions          # inert

Measured on GET / against requires :n, type: Integer:

config before after
no rescue_from 400 400
:all 500 500
:all + :grape_exceptions 400 400
StandardError 500 500
StandardError + :grape_exceptions 500 400
:grape_exceptions + StandardError 500 400
ValidationErrors + StandardError + :grape_exceptions 422 422
raising ArgumentError with the above 500 catch-all 500 catch-all

Approach

Let the opt-in win over a handler that only matched through a non-Grape ancestor. A handler registered for a Grape exception class is more precise than the opt-in and still wins, so an explicit rescue_from Grape::Exceptions::ValidationErrors keeps its handler.

registered_rescue_handler gains an _entry variant returning the matched class alongside its handler, since deciding this needs to know which class matched, not just that one did. find_handler resolves the entry once and passes it down, so there is no extra lookup.

Grape::Exceptions::InvalidVersionHeader is explicitly left alone — it must keep reaching Rack so the next versioned route is tried.

Backward compatibility

UPGRADING entry added. An API that registers a catch-all as a class and opts into :grape_exceptions will now answer Grape's own status for Grape's own errors, where it previously answered the catch-all's — which is what the opt-in asks for, and makes the two spellings of "catch everything" agree. An API that does not use rescue_from :grape_exceptions is unaffected.

Test plan

  • 4 new examples in api_spec.rb: validation stays 400 under a class catch-all, application errors still reach the catch-all, an explicit grape-class handler still wins, and a non-Grape exception is not intercepted. The last three are invariant guards against the change over-reaching; verified the behavioural one fails without the lib/ change.
  • Full RSpec suite passes locally (2547 examples, 0 failures).
  • RuboCop clean.
  • CI green.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix/grape-exceptions-precedence branch from b39fbb4 to 784fa96 Compare August 1, 2026 10:05
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

rescue_from :grape_exceptions is an opt-in to keep Grape's own errors
rendering with their own status -- a validation failure answers 400 rather
than whatever the application's catch-all returns.

It only ever worked against rescue_from :all, which lives in
all_rescue_handler and is consulted last. Written as a class instead,
rescue_from StandardError is a registered handler, matched first, and Grape's
exceptions are StandardErrors -- so the opt-in silently did nothing and
validation errors still came back as 500s:

    rescue_from StandardError { error!('server error', 500) }
    rescue_from :grape_exceptions          # inert

Let the opt-in win over a handler that only matched through a non-Grape
ancestor. A handler registered for a Grape exception class is more precise
than the opt-in and still wins, so an explicit
rescue_from Grape::Exceptions::ValidationErrors keeps its handler.

registered_rescue_handler gains an _entry variant returning the matched class
alongside its handler, since deciding this needs to know *which* class matched,
not just that one did. find_handler resolves the entry once and passes it down.

Application errors still reach the catch-all, rescue_from :all is unchanged,
and InvalidVersionHeader is left alone so it keeps reaching Rack and version
cascading still works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/grape-exceptions-precedence branch from 784fa96 to f67e86c Compare August 1, 2026 11:08
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.

1 participant