Programming - - By Tim Lucas

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.

Sponsors
Login or Create Account to Comment
Login Create Account