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.