Custom 404 action in Rails

Written . Tagged Ruby, Ruby on Rails.

By default, Rails will render public/404.html (with a 404 status in the header) when it thinks an error 404 is appropriate.

Why use a custom action?

For error 500 (Internal Server Error) pages, rendering a static file makes sense. If your app is broken enough to give that error, it may not be up to the task of rendering a dynamic error page.

But for 404 pages, a dynamic template might be preferable. You can use your site layouts (though some would argue error pages should look distinctly different) and helpers, you can suggest alternative content or try to figure out where they wanted to go, and so on.

Note that even when your 404 page is a static HTML file, it is still rendered by Rails (after failing to match a route, or some other error), so replacing it with a dynamic action adds less overhead than you might assume.

Old school: Overriding rescue_action

Blog posts on the subject that I found (like this one) suggest overriding rescue_action(_in_public).

If you go that route (or a similar one using the new-fangled rescue_from), these are the errors that Rails by default would render public/404.html for:

ActionController::RoutingError
ActionController::UnknownAction
ActiveRecord::RecordNotFound

They are enumerated in ActionController::Rescue.

New school: Change it at the source

I wanted to know exactly when Rails was rendering public/404.html, not just rescue some exceptions that I had noticed led to a 404 in production, so I dug into the code – hence the list above.

In short, ActionController::Rescue defines a rescue_action_in_public that calls render_optional_error_file.

Rather than defining my own rescue_action in ApplicationController, where I would have to duplicate the list of exceptions that should cause a 404, I defined my own render_optional_error_file there:

1
2
3
4
5
6
7
def render_optional_error_file(status_code)
  if status_code == :not_found
    render_404
  else
    super
  end
end

The render_404 method can look something like

1
2
3
4
5
6
7
def render_404
  respond_to do |type|
    type.html { render :template => "errors/error_404", :layout => 'application', :status => 404 }
    type.all  { render :nothing => true, :status => 404 }
  end
  true  # so we can do "render_404 and return"
end

render_optional_error_file is a documented method, so even though using rescue_action is probably less likely to break with future versions of Rails, I should think this solution is reasonably reliable.

Gotchas

Note that you won’t see this in local development, since it involves rescue_action_in_public. If you want to have a look, put

1
alias_method :rescue_action_locally, :rescue_action_in_public

in your ApplicationController, but don’t forget to remove it after.

Another caveat is that if your server configuration has a directive like ErrorDocument 404 /404.html (Apache) or error_page 404 /404.html; (nginx), that will eclipse your Rails action. Just remove the directive.