Quickly enable middleware based custom exception handling in Rails
I've always been dissatisfied with using rescue_from
in my ApplicationController to handle the various meaningful exceptions in my application. Its always felt more like a solution for individual types of exceptions as opposed to a broad solution to cover all uncaught exceptions with custom rendered error pages. Most documentation and help articles on the matter only go so far as to recommend the use of rescue_from
with non templated error pages (example). But Rails' middleware already has tools in place to translate exceptions not caught in your application code to the correct error response, and a formulaic way for any app to provide responses based on status code.
The ActionDispatch::ShowException middleware holds a reference to a Rack app that will respond to errors using the paths based on the status code: /400
, /404
, /500
, or etc. The default app is the ActionDispatch::PublicExceptions class which simply takes that url and attempts to return the page from public/<error_code>.html
. Any Rack app that responds to these routes can be used, however, its just a matter of configuration.
The app assigned to handle error page rendering can be set in your configuration as easily as this:
config/application.rb:
config.exceptions_app = self.routes
In this case, the middleware is being told to use your current application to render pages. When an exception is rescued it will call the exceptions_app
using the error code as the path, so you'll want to create routes and at least a controller to handle them.
config/routes.rb:
match "/400", :to => "errors#bad_request"
match "/404", :to => "errors#not_found"
match "/500", :to => "errors#internal_server_error"
These should be match
directives, not individual verbs, as these routes will need to match any type of request that encounters an error.
app/controllers/errors_controller.rb:
def bad_request
end
def not_found
end
def internal_server_error
end
From the controller you can do whatever you need to do from each of these actions. Be careful here, however, as any errors raised will not be caught and displayed in the standard way, since ActionDispatch::ShowException is expecting to be able to return what it calls a "failsafe response".
This provides middleware configuration based approach to handling errors where more than simple static pages are needed. Provide coverage for as many error codes as you find necessary, and its recommended to use a regexp based catchall in your routes to flush any error code you don't have a custom page for to a catchall error action (or respond with "X-Cascade: pass" header as PublicExceptions does):
get '/:code', to: 'errors#catch_all', constraints: {code: /^\d{3}$/}
A modular approach is also to build your custom error pages as an app independent from the main app. As mentioned, any Rack app that responds to routes like /400
, /404
, /500
, or etc can be used. Here's an example (thanks to @lazylester) of how to do this.
To me,rescue_from
feels more like a hack, while middleware that routes error page rendering through my app's controllers and views using formulaic routes feels much more integrated and congruent with the Rails request lifecycle.
More info:
- Have additional exceptions that need to be mapped to specific statuses? Merge more keys into ActionDispatch::ExceptionWrapper.rescue_responses
- Plataformatec calls this "flexible exception handling"
- Since ActionDispatch::DebugExceptions renders error debug pages in development before ShowExceptions gets a chance to render your custom error pages, you can test the rendering of your error pages by either setting
config.consider_all_requests_local = false
or by settingenv['action_dispatch.show_detailed_exceptions'] = false
in your app before the exception is raised.