October 25, 2014

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}

#{failsafe_error.backtrace * "

"}" 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.

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

152 Kudos