request.fresh? implementation does not follow HTTP specification

According to the specification, if both Last-Modified date and Entity Tags are sent, both must be validated to consider the request fresh:

http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4

Then the current implementation is wrong, because it considers the request fresh with one OR the other is fresh:

    def fresh?(response)       not_modified?(response.last_modified) || etag_matches? (response.etag)     end

The correct implementation would be:

    def not_modified?(modified_at)       modified_at && if_modified_since >= modified_at     end

    def etag_matches?(etag)       if_none_match == etag     end

    def fresh?(response)       if if_modified_since && if_none_match         not_modified?(response.last_modified) && etag_matches? (response.etag)       if if_modified_since         not_modified?(response.last_modified)       if if_none_match         etag_matches?(response.etag)       else         false       end     end

Why bother? Sometimes I want to cache a resource considering its timestamp, but this cache is only valid for the current user (works like scoping):

  def show     response.last_modified = resource.updated_at     response.etag = current_user

    if request.fresh?(response)       head :not_modified     else       # ...     end   end

In the current implementation, even the first example given by Ryan Daigle on his blog will fail:

http://www.ryandaigle.com/articles/2008/8/14/what-s-new-in-edge-rails-simpler-conditional-get-support-etags

According to the specification, if both Last-Modified date and Entity Tags are sent, both must be validated to consider the request fresh:

HTTP/1.1: Caching in HTTP

I'm not sure you're reading that paragraph correctly. It's intended to supporting both http 1.0 and 1.1 caches, if we made the change you're suggesting any 1.0 caches will ignore the etags and serve your old data. Which would defeat the purpose of its inclusion.

I believe the current behaviour is valid, it uses both validators, merely shortcircuiting once it's found a match.

def show    response.last_modified = resource.updated_at    response.etag = current_user

   if request.fresh?(response)      head :not_modified    else      # ...    end end

In the current implementation, even the first example given by Ryan Daigle on his blog will fail:

You should include the user id in the etag along with the other freshness information.

According to the specification, if both Last-Modified date and Entity Tags are sent, both must be validated to consider the request fresh:

You're right. The relevant part from the spec is: "An HTTP/1.1 caching proxy, upon receiving a conditional request that includes both a Last- Modified date and one or more entity tags as cache validators, MUST NOT return a locally cached response to the client unless that cached response is consistent with all of the conditional header fields in the request."

I'll get it changed today.

DHH,

Considering this, we also have to change automatic etagging. We should not automatically set etag (with body digest) when Last- Modified field is set, because to check this etag, we would have to render the action and this is what we want to avoid in first place.

And the current implementation of conditional get (handler_conditional_get!) automatically replies 304 whenever the etag is matching (even if it's not body digest).

I believe this might be the right implementation:

      def handle_conditional_get!         if etag? || last_modified?           set_conditional_cache_control!         elsif nonempty_ok_response?           self.etag = body           if request && request.etag_matches?(etag)             self.status = '304 Not Modified'             self.body = ''           end           set_conditional_cache_control!         end       end

I can create a patch if needed.

Thanks,

Considering this, we also have to change automatic etagging. We should not automatically set etag (with body digest) when Last- Modified field is set, because to check this etag, we would have to render the action and this is what we want to avoid in first place.

You're right. I've added your implementation and tests for it. Thanks for helping to sanity check all this.

I'm glad to help! =)

I was checking those things because I'm writing some code that relies on such features. It uses classes methods to do HTTP Cache:

  class ListsController < ApplicationController     http_cache :index, :show, :last_modified => :list, :etag => :current_user

    protected       def list         @list ||= List.find(params[:id])       end

      def current_user         @current_user ||= User.find(params[:user_id])       end   end

It's very lightweight (100 lines) and accepts also Procs, Methods and Symbols as parameters. If there is some interest in merging into Rails Core, I will be glad to help again.

README and source code are here: http://github.com/josevalim/easy-http-cache/

Regards,