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.

Rails 500 Internal Server Error An Internal Server Error in production

Rails 404 Record Not Found Record Not Found in production mode

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
app/controllers/application_controller.rb (Rails 2.3.x)

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
config/routes.rb

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) }
config/application.rb

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
app/controller/exceptions_controller.rb

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?

Comments


Avatar
Stan Rawrysz – 8 months ago

Awesome. This really helped me get what I needed. Nice to see this feature in 3.2.

Avatar
– 8 months ago

This works, but it's causing my rspec tests to fail and I can't figure out why:

context 'when attempting to edit another record' do

  it 'should return http forbidden' do

    put :update, :id => 12342343343

    response.should be_forbidden

  end

end

The result I get is:

1) UsersController PUT update when attempting to edit another record should return http forbidden
     Failure/Error: put :update, :id => 12342343343
     Error::Forbidden:
       Error::Forbidden
     # ./app/controllers/application_controller.rb:16:in `render_forbidden'
     # ./app/controllers/users_controller.rb:50:in `validate_user'
     # ./spec/controllers/users_controller_spec.rb:174:in `block (4 levels) in <top (required)>'

Here's what my config looks like:

config.action_dispatch.rescue_responses['Error::Forbidden'] = :forbidden
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }

And here's ErrorsController#show:

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]

  render :action => @rescue_response, :status => @status_code, :formats => [:html]
end

When I hit the url with a browser, I get the 403 response code as expected...it just fails in rspec.

Any ideas?

First sign in through Github or Twitter