Being a good little 404er

Before I post about how to add fragment caching I’d like to share this, in case people haven’t seen it. I remember seeing this technique in Rick Olson‘s code a while ago, so full credit to him.

ActiveRecord raises an RecordNotFound exception if it can’t find the database record with the ID you requested.


def show
  @person = Person.find(params[:id])
end

If params[:id] doesn’t match a database record an ActiveRecord::NotFound exception will be raised. Using ActionController’s rescue_action_in_public method we can capture these exceptions and throw 404′s accordingly, and for me at least, this covers about 99% of use cases.

To do this application wide, add the following protected method to your ApplicationController.rb


def rescue_action_in_public(e)
  if e.is_a? ActiveRecord::RecordNotFound
    render :file => "#{RAILS_ROOT}/public/404.html",
           :status => '404 Not Found'
  else
    super
  end
end

In reality I usually move that render call out into a render_404 method, so you can handle HTML, XML and any other types of requests. It also allows you to call it from within the controller subclasses if needed.


def render_404
  respond_to do |format|
    format.html { render :file => "#{RAILS_ROOT}/public/404.html", :status => '404 Not Found' }
    format.xml  { render :nothing => true, :status => '404 Not Found' }
  end
  true
end

def rescue_action_in_public(e)
  case e when ActiveRecord::RecordNotFound
    render_404
  else
    super
  end
end

To be a good little 404er with the above code all you need to do is ensure that your important database calls throw this exception, which brings me to an important point: as of Rails 1.1, the only type of find that raises a RecordNotFound exception is a find by id (read the first paragraph of documentation on the the find method). If you’re doing a different kind of find you have to handle it yourself:


def show
  @tag = Tag.find_by_name(params[:name]) or raise ActiveRecord::RecordNotFound
end

The same thing applies (i.e. raising your own exception) with find(:all) and find(:first) as well.

If you want to throw a 404 directly you can just do:


def show
  @tag = Tag.find_by_name(params[:name]) or (render_404 and return)
end

There’s discussion on the rails-core list about adding a find! method, which would make this approach even cleaner.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.igvita.com/blog igrigorik

    It’s brilliant! I must have been looking at the same code as you were because I found the article after I started ‘googling’ for more information on this technique.

    Anyway, just a little tip/pointer for anyone who is thinking of using this:
    – rescue_action_in_public is for requests answering false to local_request? (From the documentation). Meaning, if you’re trying to test this on your local machine, it will still render the default exception template with RecordNotFound info on it. Seems obvious once you know it, but it took me solid 10 minutes to figure out why I ‘didn’t seem to work on my machine’.

    Replacing rescue_action_in_public with rescue_action will redirect/render on your local machine/dev environment also.

    Cheers,
    Ilya

  • http://www.toolmantim.com timlucas

    I love it when that happens!

    Thanks for pointing out that rescue_action_in_public only happens in production. If you want to emulate it in dev you can define rescue_action_locally and call the other rescue, as follows:

    
    def rescue_action_locally(e); rescue_action_in_public(e); end
    
  • niko

    And i was even using find(:first, :condition=>{… to make bogus find-operation _not_ raise an error but return false which i handled afterwards. Your method is so much more ellegant. Thank you, Tim.

  • niko

    Hm… little problem: although i used “rescue_action” and everything works in dev mode my tests fail:

    get :show, {:id=>42, :path=>”somepage.html”}
    assert_response 404

    gives me:

    Couldn’t find Page with ID=42

    Any idea?

  • niko

    OK. Got it. At the beginning of the controller_test stubs there is this line:

    # Re-raise errors caught by the controller.
    class PageController; def rescue_action(e) raise e end; end

    Of course i had to uncomment the redefinition of rescue_action to make the tests pass. Is there any bad side-effects in doing this?

    More cheers, Niko. :)

  • Danger

    Rick Olson also has made use of the “save!” method through similar means. He’s the only guy I’ve seen actually use save! (which raises an error if the save fails) and have his rescue_action catch it. Very slick.

  • nicolas

    … i’m not so sure…
    ..that 404 is really adequate, after all “Page not found” is not the same as “Record not found”.
    It may be ok in the case of a “show” action, but it can also occur in “update” and “destroy” actions where “Page not found” doesn’t make any sense whatsoever.
    Think of a raise-condition where 2 people handling the same record and one person deletes the record and the other one trys to delete/update after and boom…
    This maybe a rare situation but still mixing “Page not found” and “Record not found” feels wrong to me.
    So where as the concept is a good one (putting the Rescue into the ApplicationController) the actual action taken seems to be the wrong one.

  • Pingback: Raise Error404, don't render and return – The Pug Automatic