Diving in Rails - Exceptions handling

Ruby code, just like any other code, can be subject to failure.

Fortunately, when an error occurs inside your Rails application, your rails server does not crash and stop serving requests.

In this article, I’ll explain how rails catches exceptions that may happen in your code, and how it renders a nice error page, depending on the Rails environment, and how you can customise it.

I won’t be talking about rescue_from, and ActionController::Rescue, but about the rack part.

 Remember, it’s all Rack !

If you read my previous article about [Rails request handling](blog.siami.fr/diving-in-rails-the-request-handling), you know that Rails is based on Rack, and uses middlewares for various things.

Rails also handles exceptions with middlewares.

This article will explain how exceptions are handled in a production environment. If you are interested in understanding how Rails displays errors in development environment, have a look at the ActionDispatch::DebugExceptions middleware.

The main middleware responsible for exception rescuing is ActionDispatch::ShowExceptions

Let’s have a look at the call method :

def call(env)
  @app.call(env)
rescue Exception => exception
  if env['action_dispatch.show_exceptions'] == false
    raise exception
  else
    render_exception(env, exception)
  end
end

Pretty simple, call the Rack app, and rescue all exceptions.

If env['action_dispatch.show_exceptions'] is false, the exception is re-raised. It is configurable by setting :

config.action_dispatch.show_exceptions = false

So, what happens if the error is re-raised ? Will the server just … stop ?

No. Fortunately, Web servers will catch the exceptions themselves as well.

Some have their own error handlers, and some others may use Rack::ShowExceptions that will just show a backtrace and some informations.

 More rack !

Now let’s have a look at the render_exception method :

def render_exception(env, exception)
  wrapper = ExceptionWrapper.new(env, exception)
  status  = wrapper.status_code
  env["action_dispatch.exception"] = wrapper.exception
  env["PATH_INFO"] = "/#{status}"
  response = @exceptions_app.call(env)
  response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
rescue Exception => failsafe_error
  $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
  FAILSAFE_RESPONSE
end

def pass_response(status)
  [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
end

First, we create an ExceptionWrapper, it is used to retrieve a status code corresponding to the exception.

For example, if we try to find a non-existing ActiveRecord model, rails will raise a ActiveRecord::RecordNotFound.

ExceptionWrapper will look into config.action_dispatch.rescue_responses to get an appropriate status code for this error. Since it’s a content not found, in this case it will be a 404 status code.

Then, rails adds the exception into the rack env and overwrites the PATH_INFO variable, which represents the URL the browser is calling. It replaces the URL with the status code.

For example, in the case I described just before, the PATH_INFO env variable would be changed to “/404”

Then, we have this line :

response = @exceptions_app.call(env)

More rack !

When an error is raised, Rails will forward the request to a rack app, and change some environment variables in order to indicate the status code and the exception.

If the rack app decides to pass the request by sending a X-Cascade header, Rails will display en empty page.

@exceptions_app is set when building the middleware in the default exception stack :

def build_stack
  # ...
  middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
end

def show_exceptions_app
  config.exceptions_app ||
  ActionDispatch::PublicExceptions.new(Rails.public_path)
end

By default, the rack app used to show exceptions is ActionDispatch::PublicExceptions, let’s have a look !

module ActionDispatch
  class PublicExceptions
    attr_accessor :public_path

    def initialize(public_path)
      @public_path = public_path
    end

    def call(env)
      status       = env["PATH_INFO"][1..-1]
      request      = ActionDispatch::Request.new(env)
      content_type = request.formats.first
      body         = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) }

      render(status, content_type, body)
    end

    private

    def render(status, content_type, body)
      format = "to_#{content_type.to_sym}" if content_type
      if format && body.respond_to?(format)
        render_format(status, content_type, body.public_send(format))
      else
        render_html(status)
      end
    end

    def render_format(status, content_type, body)
      [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
                'Content-Length' => body.bytesize.to_s}, [body]]
    end

    def render_html(status)
      found = false
      path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
      path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))

      if found || File.exist?(path)
        render_format(status, 'text/html', File.read(path))
      else
        [404, { "X-Cascade" => "pass" }, []]
      end
    end
  end
end

The role of this middleware is to find a file in the public directory to render the exception, and possibly to find a file in the correct locale.

For example, when issuing a 500 error, this middleware will display the file public/500.html

The call method will get the status from the path info, get the content type of the request thanks to ActionDispatch::Request and prepare a hash containing the status and a human readable message in case the request is in another format than HTML in order to render it instead of the HTML file.

 Customisation

It’s possible to use a different app to render exceptions, by setting config.exceptions_app in application.rb or in an environment config file.

Some people want to render very specific errors and need an access to rails goodness when rendering those pages.

It’s possible to go meta and assign our own rack app (the rails app) as our exception handling app, so when an exception arises, the corresponding status code will be called right into our app :

# Application.rb
config.exceptions_app = self.routes

# routes.rb

match '/500' => 'errors#default'
match '/404' => 'errors#missing'

# errors_controller.rb

def default
  # Specific code
end

def missing
  # Specific code
end
 
127
Kudos
 
127
Kudos

Now read this

Add some regular ruby code in your rails migrations

For long, I’ve used rails migrations only as a way to update my database schema, no more. Today, I had to do a more complex operation. I have a Message model and a Conversation model; for historical reasons, the read / unread was stored... Continue →