What's New in Edge Rails: Simpler Conditional Get Support (ETags)

Posted by ryan
at 8:32 PM on Wednesday, August 13, 2008

Note: This feature has been greatly improved since the writing of this article. See here for the latest and greatest.

Conditional-gets are a facility of the HTTP spec that provide a way for web servers to tell browsers that the response to a GET request hasn’t changed since the last request and can be safely pulled from the browser cache.

They work by using the HTTP_IF_NONE_MATCH and HTTP_IF_MODIFIED_SINCE headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server’s version then the server only needs to send back an empty response with a not modified status.

It is the server’s (i.e. our) responsibility to look for a last modified timestamp and the if-none-match header and determine whether or not to send back the full response. With this new conditional-get support in rails this is a pretty easy task:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ArticlesController < ApplicationController

  def show
    @article = Article.find(params[:id])

    # Set the response headers to accurately reflect the state of the
    # requested object(s)
    response.last_modified = @article.published_at.utc
    response.etag = @article

    # If the request's state is the same as the server's state then we know
    # we don't have to send back the whole body
    if request.fresh?(response)
      head :not_modified
    else
      respond_to do |wants|
        # normal response processing
      end
    end
end

The etag value is calculated for you with the etag= setter method. All you have to do is provide a single object or array of objects that uniquely identify this request. In this example the article itself contains all the information that uniquely identifies the state of this request. However, you may need to use more than one key in your app. For instance, if the request is user specific:


response.etag = [@article, current_user]

The request.fresh?(response) method is what will then tell you if the incoming request matches either the last-modified-since or if-none-match values of the outgoing response. If it does you can avoid passing the full body of the response back and save some bandwidth.

It’s also possible that you can avoid hitting the database all together if your application deals with completely static resources (though this is rare):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ArticlesController < ApplicationController

  def show

    # If articles don't change, the etag can be based solely
    # on items we have in the request
    response.etag = [:article, params[:id]]

    # If the request's state is the same as the server's state then we can
    # avoid the db call all together
    if request.fresh?(response)
      head :not_modified
    else
      @article = Article.find(params[:id])
      respond_to do |wants|
        ...
      end
    end
end

So be a good citizen and make your requests conditional-get compatible. It’s the right thing to do – and can make your apps more performant.

tags: ruby, rubyonrails

Comments

Leave a response

  1. Ben WoodcroftAugust 13, 2008 @ 09:26 PM

    Just wondering, what happens when the code serving it changes? Does the fresh?() method take that into account somehow?

    Otherwise we could potentially say that an old page has not changed because the data didn’t change (and hence the etag doesn’t change), when it did change because the html representation is different.

    Is that our responsibility to add a code version in the etags or something?

  2. Alexey KovyrinAugust 13, 2008 @ 10:34 PM

    @Ben: Afaiu, you can use something like SVN rev or last deployment time as a part of ETag.

  3. Eric AndersonAugust 14, 2008 @ 06:01 AM

    I really like this feature. Conditional get support is always such a pain to use. This makes it much easier. My only complaint is between this and the REST stuff controllers are getting very repetitive and ugly.

    Thankfully we have make_resourceful to help us with this. It should be pretty easy to make a proc that can be included into the make_resourceful call that will do most of the work for us instead of repeating the logic in every controller.

  4. Hongli LaiAugust 14, 2008 @ 06:03 AM

    Very cool!

  5. leethalAugust 14, 2008 @ 06:58 AM

    What about something like this, instead of stating head :not_modified all the time? http://pastie.org/252869

    I didn’t fully grok this post, actually, so the ‘in_modified_scope’ method name probably isn’t the best. But using a block instead of stating head :not_modified would be nice.

  6. ryanAugust 14, 2008 @ 07:57 AM

    leethal – this pattern is totally ripe for abstraction, and I think your in_modified_scope method makes a lot of sense. Perhaps a name like when_new_request would be better? Not sure, but the idea is sound.

  7. Skip HireAugust 14, 2008 @ 11:58 AM

    I am a PHP programmer considering moving over to R/RoR. I really like the look of how you are handeling this. In PHP/Apache i totally overwrote the handling of GET & POST.

  8. LennonAugust 14, 2008 @ 06:54 PM

    I think you can use a block (as in leethal’s example) and simplify even further by passing the etag value to the utility method, and defining it in your application controller:

    http://pastie.org/253302

  9. Dan KubbAugust 14, 2008 @ 08:35 PM

    Could this feature allow you to handle Conditional PUT and DELETE requests? They aren’t used often, but are extremely useful. Basically they are a way of saying “only update or delete the resource if it matches what I last requested”. The idea is that the server can prevent a resource from being updated or deleted based on stale data the client has.

  10. Dan KubbAugust 14, 2008 @ 10:00 PM

    I should add to my previous comment, Conditional PUT and DELETE use the If-Match and If-Unmodified-Since headers, and the server returns a 412 Precondition Failed status when the server state has changed, but the idea is similar: only process the request if the resource hasn’t changed since the last request.

  11. Stephen TousetAugust 15, 2008 @ 11:25 AM

    The implementation of this is horrible. A plugin to do exactly this already exists. Why can’t the Rails team just incorporate the work of others, instead of rewriting things that have already been written?

    http://blog.labnotes.org/2007/12/14/if_modified-second-round/

  12. GuyAugust 19, 2008 @ 01:15 AM

    Very cool stuff, I am just now starting to learn it but the ETags are awesome addition!

  13. GlennAugust 20, 2008 @ 07:44 AM

    Interesting stuff. I’ll try find a clean way to abstract it if someone doesn’t before me, although I think it should be relatively easy to patch make_resourceful & resourceful_controller to cover the common cases quickly

  14. José ValimOctober 21, 2008 @ 10:11 AM

    Today, DHH just pushed some new changes into Rails HTTP cache mechanism. Ryan, your examples might be outdated! =)

    Besides, I’ve just finished Easy HTTP Cache plugin to use those last changes. Instead of working inside your action, you can do:

    class ListsController < ApplicationController
      http_cache :show, :last_modified => :list, :etag => :current_user
    end
    def show
      # expensive stuff
    end
    protected
      def list
        @list ||= List.find(params[:id])
      end
    def current_user
      @current_user ||= User.find(params[:user_id])
    end

    Available at GitHub: http://github.com/josevalim/easy-http-cache