Exception Applications in Rails 3.2
No Ruby on Rails developer want to see them in production: Exceptions! By default every Rails project comes with two static files that get rendered when an exception in production occurs.
The problem with these pages is, that they do not fit well into any design and do not tell the user what really went wrong. In Rails 2.x it was possible to catch all thrown exceptions using the rescue_from
method in the ApplicationController.
class ApplicationController < ActionController::Base
rescue_from(ActionController::RoutingError) {
render :template => 'errors/404'
}
end
In Rails 3 however routing was extracted into its own middleware called ActionDispatch. While this is a good thing, it has created a lot of confusion on how to handle ActionController::RoutingError
. Having routing done by a middleware means that in case of a routing error the ApplicationController will not get executed, and therefore rescue_from
can not handle any routing errors.
Instead ActionDispatch
provides a class ActionDispatch::ShowException
to handle exceptions that happen in the middleware.
Over time the community has proposed several solutions to solve this problem.
Override ShowException
Matthew Gibbons proposed in his article Rails 3.0 Exception Handling to override the ShowException class in an initializer. The overriden method render_exception
will call a method on a custom ErrorsController and render out the exception in the layout of the page.
The advantage of this approach is that the user will see the error message rendered in the layout of the page and it is up to you on how to properly present the error message to the user. While this was a viable solution for Rails 3.0 and 3.1, in Rails 3.2 it will not work anymore.
Catch all route
Tian from TechOctave proposed in his article Rails 3.0 rescue from Routing Error Solution to introduce a catch all route in config/routes.rb
.
Yourapp::Application.routes.draw do
#Last route in routes.rb
match '*a', :to => 'errors#routing'
end
He mentions that the a
"is actually a parameter in the Rails 3 Route Globbing technique. For example, if your url was /this-url-does-not-exist, then params[:a]
equals /this-url-does-not-exist
". While this solution looks nice at first, it will create problems when your app uses engines.
Exception Application
The release notes of Rails 3.2 introduce a new solution to the workarounds above.
Added
config.exceptions_app
to set the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).
The best solution I have come across so far which utilizes the new config setting was proposed by Sjoerd Andringa. He uses a lambda expression for the setting, which gets called when an exception happens in the middleware.
config.exceptions_app = ->(env) { ExceptionsController.action(:show).call(env) }
The use of a lambda expression is necessary, because the controller name constant is not yet available in the initialization stage. What lambda expression does, is when ActionDispatch
catches an exception, it will invoke the lambda expression from the config with the environment variable as parameter and then call the show action on the ExceptionsController
.
class ExceptionsController < ActionController::Base
layout 'application'
def show
@exception = env['action_dispatch.exception']
@status_code = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
@rescue_response = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
respond_to do |format|
format.html { render :show, status: @status_code, layout: !request.xhr? }
format.xml { render xml: details, root: "error", status: @status_code }
format.json { render json: {error: details}, status: @status_code }
end
end
protected
def details
@details ||= {}.tap do |h|
I18n.with_options scope: [:exception, :show, @rescue_response], exception_name: @exception.class.name, exception_message: @exception.message do |i18n|
h[:name] = i18n.t "#{@exception.class.name.underscore}.title", default: i18n.t(:title, default: @exception.class.name)
h[:message] = i18n.t "#{@exception.class.name.underscore}.description", default: i18n.t(:description, default: @exception.message)
end
end
end
helper_method :details
end
The controller uses env["action_dispatch.exception"]
in order to retrieve the original exception object that was raised to get the status code and message and then renders out the show view, which you can design as want. Note the first line that sets the layout in the controller to the application layout, so all views will be rendered in the same way as the rest of your app.
The original source, including the i18n file, can be found as a Gist on Github.
Conclusion
We have seen a few solutions to the new way of handling exceptions in Rails 3. For Rails 3.2 and up it is definitely a good solution to configure an exceptions_app and handle exceptions using a custom ExceptionsController.
What strategy do you use in your production app?