rails 4: custom error pages for 404, 500 and where is the default 500 error message coming from?

Update 2018

Our exception_handler gem is now considered the most popular (Rails custom error pages) ↴


How It Works

All Rails exceptions are handled with config.exceptions_app. This is assigned in the config/application.rb or config/environments/*.rb files - it needs to be a callback:

config.exceptions_app sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

Whenever Rails hits an error, it invokes the ShowExceptions middleware. This calls exception_app and sends the entire request (including exception) to the exceptions_app:

Middleware-Powered Exceptions

exceptions_app needs to deliver a response. If not, the failsafe is loaded:

  # show_exceptions.rb#L38
  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(request.env) # => exceptions_app callback
    response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
  rescue Exception => failsafe_error # => raised if exceptions_app false
    $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
    FAILSAFE_RESPONSE
  end

The failsafe is stored as FAILSAFE_RESPONSE at the top of ShowExceptions.


Custom Error Pages

If you want to create custom error pages, you need to inject your own callback into config.exceptions_app. This can either be done in application, or with a gem:

enter image description here

Notice how the call method is used - this is how a callback works. Rails (env) is invoked when the request is received from the Internet; when an exception is raised, env is passed to exceptions_app.

The quality of your exception handling will be dependent on how you manage env. This is important; referencing self.routes does not carry the environment forward.

The best way is to handle exceptions with a separate controller. This allows you to handle the request as if it was just another view, granting access to the layout and other components (model / email).

--

There are two ways to handle exceptions:

  1. Overriding 404 / 500 routes
  2. Invoking a controller

Our gem was designed around our controller - invoked each time an exception is raised. This gives full control over the exception process, allowing for 100% branded layout:

enter image description here

ExceptionHandler is now the leading production custom error pages gem for Rails.

Maintained for over 3 years, it is the simplest and most powerful exception gem for Rails. It works 100% on Rails 5 and has already been downloaded over 70,000 times.


Gem

Latest ver 0.8.0.0 has following updates:

  • Custom exceptions
  • Exception "mapping" (choose which exceptions to handle)
  • Email notifications
  • Model backend
  • Sprockets 4+ integration
  • RSpec Test Suite
  • Locale-based views

You can read more here.


Managing Rails' Exceptions

If you're not interested in the gem, let me explain the process:

All Rails exceptions are handled with the config.exceptions_app callback. This is assigned in the config/application.rb or config/environments/*.rb files - it needs to be a callback:

config.exceptions_app sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

Whenever an exception is raised by your app, the ShowExceptions middleware is invoked. This middleware builds the exception into the request and forwards it to the config.exceptions_app callback.

By default, config.exceptions_app points to the routes. This is why Rails comes with 404.html, 500.html and 422.html in the public folder.

If you want to create custom exception pages, you need to override the config.exceptions_app callback - passing the erroneous request to an appropriate handler, be it a controller or route:

[[ middleware ]]

The two ways to manage this effectively are either to send the erroneous requests to the routes, or to invoke a controller.

The simplest - and most common - way is to forward the request to the routes; unfortunately, this ignores the request and prevents you from detailing the exceptions properly.

The best way is to invoke a separate controller. This will allow you to pass the entire request, allowing you to save it, email it or do a number of other things.

--

400 / 500 Errors

Rails can only respond with HTTP-valid errors.

Whilst the app's exception may be different, the returned status code should be either 40x or 50x. This is in line with the HTTP spec, and outlined here.

This means that no matter what exception handling solution you use/build, Rails needs to return either 40x or 50x errors to the browser.

In other words, custom error pages have little to do with the type of exception - more how you're catching and serving the browser response.

By default, Rails does this with 404.html, 422.html and 500.html files in the public folder. If you want to handle the exception flow yourself, you need to remove these files and channel the erroneous requests to your own exceptions_app callback.

This can be done with the routes or with a controller (which I'll explain now):


1. Routes

The simplest way is to let the routes handle it.

This method is bloated and requires using multiple actions. It is also difficult to manage the responses.

This shows how to replace the exceptions_app with the routes directly:

# config/application.rb
config.exceptions_app = self.routes

Here is the code I have (Ruby 2.0.0, Rails 4.0):

Application Config

#config/application.rb
config.exceptions_app = self.routes

Routes

#config/routes.rb
if Rails.env.production?
   get '404', to: 'application#page_not_found'
   get '422', to: 'application#server_error'
   get '500', to: 'application#server_error'
end

Application Controller

#controllers/application_controller.rb
def page_not_found
    respond_to do |format|
      format.html { render template: 'errors/not_found_error', layout: 'layouts/application', status: 404 }
      format.all  { render nothing: true, status: 404 }
    end
  end

  def server_error
    respond_to do |format|
      format.html { render template: 'errors/internal_server_error', layout: 'layouts/error', status: 500 }
      format.all  { render nothing: true, status: 500}
    end
  end

Errors Layout (totally static -- for server errors only)

#views/layouts/error.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= action_name.titleize %> :: <%= site_name %></title>
  <%= csrf_meta_tags %>
  <style>
    body {
        background: #fff;
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
    }
    .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
    }
    .error_container .error {
        display: block; 
        text-align: center;
    }
    .error_container .error img {
        display: block;
        margin: 0 auto 25px auto;
    }
    .error_container .message strong {
        font-weight: bold;
        color: #f00;
    }
  </style>
</head>
<body>

    <div class="error_container">
        <%= yield %>
    </div>

</body>
</html>

Error Views

#views/errors/not_found_error.html.erb    
<div class="error">
    <h2>Sorry, this page has moved, or doesn't exist!</h2>
</div>


#views/errors/internal_server_error.html.erb
<div class="error">
    <div class="message">
        <strong>Error!</strong>
        We're sorry, but our server is experiencing problems :(
    </div>
</div>

Whilst many prefer the "routes" method for its simplicity, it is neither efficient or modular. Indeed, if your application has any semblance of object orientation, you'll quickly dismiss it as a hack.

A much more resounding way is to use a custom controller to catch the pure exception. This way, you can construct the flow in accordance with your application's overall structure:


2. Controller

The other option is to route all the requests to a controller.

This is infinitely more powerful as it allows you to take the request (exception) and pass it through to the views, whilst managing it in the backend. This will allow for the likes of saving it to the database.

This gist shows how.

It means we can hook into the middleware & pass the entire request to a controller.

If this controller is backed by a model and views, we can extract it into a gem (which is what we did). If you wanted to do it manually, here's how:

--

Config

The beauty of this method is that it hooks directly into config.exceptions_app. This means any exception can be handled natively, allowing for more efficiency. To make sure this works, you need to put the following code into config/application.rb (exceptions_app only works in production - development shows the errors):

#config/application.rb
config.exceptions_app = ->(env) { ExceptionController.action(:show).call(env) }

To test, you can set the "local" requests to false:

#config/environments/development.rb
config.consider_all_requests_local  = false # true

--

Controller

The next step is to add an exception controller. Whilst this can be handled in application_controller, it is far better to extract into its own. Notice the call from the application.rb -- ExceptionController.action(:show):

#app/controllers/exception_controller.rb
class ExceptionController < ApplicationController

  #Response
  respond_to :html, :xml, :json

  #Dependencies
  before_action :status

  #Layout
  layout :layout_status

  ####################
  #      Action      #
  ####################

  #Show
  def show
    respond_with status: @status
  end

  ####################
  #   Dependencies   #
  ####################

  protected

  #Info
  def status
    @exception  = env['action_dispatch.exception']
    @status     = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
    @response   = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
  end

  #Format
  def details
    @details ||= {}.tap do |h|
      I18n.with_options scope: [:exception, :show, @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

  ####################
  #      Layout      #
  ####################

  private

  #Layout
  def layout_status
    @status.to_s == "404" ? "application" : "error"
  end

end

--

Views

There are two views to add to get this working.

The first is the exception/show view, and second is the layouts/error. The first is to give the exception_contoller#show a view, and the second for 500 internal server errors.

#app/views/exception/show.html.erb
<h1><%= details[:name]    %></h1>
<p><%=  details[:message] %></p>


#app/views/layouts/error.html.erb (for 500 internal server errors)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <title>Error</title>
    <style>
      html {
        height: 100%;
        background: #fff;
      }
      body {
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
      }
      .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
      }
      .error_container .error {
        display: block;
        text-align: center;
      }
      .error_container .error img {
        display: block;
        margin: 0 auto 15px auto;
      }
      .error_container .message > * {
        display: block;
      }
      .error_container .message strong {
        font-weight: bold;
        color: #f00;
      }
    </style>
  </head>
  <body>
    <div class="error_container"><%= yield %></div>
  </body>
</html>

Conclusion

The exception doesn't matter as much as the error code.

When Rails raises an exception, it assigns one of the above HTTP response codes. These allow your browser to determine whether the request was successful.

When dealing with exceptions, you need to ensure you're able to handle 40* errors (which will typically use the same layout as the rest of your app) and the 50* errors (which will need their own layout).

In both cases, you'll be best using a separate exception controller, which will allow you to manage the exception as an object.


The Error you are experiencing is being thrown from

https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L18-L22

This means, the code your exceptions are getting rescued by are themselves throwing exceptions. You can check your logs for text:

Error during failsafe response:

to identify what the exceptions really are originating from and thus solve your problem.


Error pages in application should be as simple as it possible. Same recommendation concerns their rendering. If your application returns 500 HTTP response code it means that things got wrong already. And there is a chance you could not render error page and display it to user.

Ideally error pages should be a plain HTML served directly by your web server without hitting application server.

Speaking of Rails implementation of this idea. It based on using asset pipeline for precompiling the HTML static pages.

Firstly add new assets type (Rails > 4.1):

# config/initializers/assets.rb

Rails.application.config.assets.precompile += %w(404.html 500.html)
Rails.application.config.assets.paths << Rails.root.join('app/assets/html')
Rails.application.config.assets.register_mime_type('text/html', '.html')

If templating engine is using (e.g. slim, haml), register it via initializer:

# for Slim
Rails.application.assets.register_engine('.slim', Slim::Template)
# for Haml
Rails.application.assets.register_engine('.haml', Tilt::HamlTemplate)

Now you ready to create pretty error pages in app/assets/html directory using your favorite template engine and Rails built-in view helpers.

Tips for production

On production asset pipeline adds digest to compiled assets and stores files under default folder (typically shared/public/assets on production server). You can use capistrano to copy error pages to web server root:

# config/deploy.rb
# Capistrano 3 only

namespace :deploy do
  desc 'Copy compiled error pages to public'
  task :copy_error_pages do
    on roles(:all) do
      %w(404 500).each do |page|
        page_glob = "#{current_path}/public/#{fetch(:assets_prefix)}/#{page}*.html"
        # copy newest asset
        asset_file = capture :ruby, %Q{-e "print Dir.glob('#{page_glob}').max_by { |file| File.mtime(file) }"}
        if asset_file
          execute :cp, "#{asset_file} #{current_path}/public/#{page}.html"
        else
          error "Error #{page} asset does not exist"
        end
      end
    end
  end
  after :finishing, :copy_error_pages
end

And last thing. Tell web server to use these files for certain HTTP error codes (sample nginx configuration):

error_page 500 502 503 504 /500.html;    
error_page 404 /404.html;

Sprocket 3 Update

For Sprocket 3 you need something like this (tested with Rails 5):

# config/environments/production.rb
config.assets.configure do |env|
  env.register_transformer 'text/slim', 'text/html', Slim::Template
  env.register_mime_type 'text/slim', extensions: ['.html']
  env.register_engine '.slim', Slim::Template
end

# config/initializers/assets.rb
Rails.application.config.assets.precompile += %w(404.html 500.html)
Rails.application.config.assets.paths << Rails.root.join('app/assets/html')