Programming
Article
By Tim Lucas

Being a good little 404er

By Tim Lucas

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.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account