lib/rack/cache/headers.rb in rack-cache-0.2.0 vs lib/rack/cache/headers.rb in rack-cache-0.3.0

- old
+ new

@@ -19,11 +19,11 @@ # Valueless parameters (e.g., must-revalidate, no-store) have a Hash value # of true. This method always returns a Hash, empty if no Cache-Control # header is present. def cache_control @cache_control ||= - (headers['Cache-Control'] || '').split(/\s*,\s*/).inject({}) {|hash,token| + headers['Cache-Control'].to_s.split(/\s*[,;]\s*/).inject({}) {|hash,token| name, value = token.split(/\s*=\s*/, 2) hash[name.downcase] = (value || true) unless name.empty? hash }.freeze end @@ -77,13 +77,23 @@ # HTTP response header helper methods. module ResponseHeaders include Rack::Cache::Headers - # Set of HTTP response codes of messages that can be cached, per - # RFC 2616. - CACHEABLE_RESPONSE_CODES = Set.new([200, 203, 300, 301, 302, 404, 410]) + # Status codes of responses that MAY be stored by a cache or used in reply + # to a subsequent request. + # + # http://tools.ietf.org/html/rfc2616#section-13.4 + CACHEABLE_RESPONSE_CODES = [ + 200, # OK + 203, # Non-Authoritative Information + 300, # Multiple Choices + 301, # Moved Permanently + 302, # Found + 404, # Not Found + 410 # Gone + ].to_set # Determine if the response is "fresh". Fresh responses may be served from # cache without any interaction with the origin. A response is considered # fresh when it includes a Cache-Control/max-age indicator or Expiration # header and the calculated age is less than the freshness lifetime. @@ -95,28 +105,28 @@ # with the origin before use. This is the inverse of #fresh?. def stale? !fresh? end - # Determine if the response is worth caching under any circumstance. An - # object that is cacheable may not necessary be served from cache without - # first validating the response with the origin. + # Determine if the response is worth caching under any circumstance. Responses + # marked "private" with an explicit Cache-Control directive are considered + # uncacheable # - # An object that includes no freshness lifetime (Expires, max-age) and that - # does not include a validator (Last-Modified, Etag) serves no purpose in a - # cache that only serves fresh or valid objects. + # Responses with neither a freshness lifetime (Expires, max-age) nor cache + # validator (Last-Modified, Etag) are considered uncacheable. def cacheable? return false unless CACHEABLE_RESPONSE_CODES.include?(status) - return false if no_store? + return false if no_store? || private? validateable? || fresh? end # The response includes specific information about its freshness. True when # a +Cache-Control+ header with +max-age+ value is present or when the # +Expires+ header is set. def freshness_information? - header?('Expires') || !cache_control['max-age'].nil? + header?('Expires') || + !!(cache_control['s-maxage'] || cache_control['max-age']) end # Determine if the response includes headers that can be used to validate # the response with the origin using a conditional GET request. def validateable? @@ -125,60 +135,102 @@ # Indicates that the response should not be served from cache without first # revalidating with the origin. Note that this does not necessary imply that # a caching agent ought not store the response in its cache. def no_cache? - !cache_control['no-cache'].nil? + cache_control['no-cache'] end # Indicates that the response should not be stored under any circumstances. def no_store? cache_control['no-store'] end + # True when the response has been explicitly marked "public". + def public? + cache_control['public'] + end + + # Mark the response "public", making it eligible for other clients. Note + # that responses are considered "public" by default unless the request + # includes private headers (Authorization, Cookie). + def public=(value) + value = value ? true : nil + self.cache_control = cache_control. + merge('public' => value, 'private' => !value) + end + + # True when the response has been marked "private" explicitly. + def private? + cache_control['private'] + end + + # Mark the response "private", making it ineligible for serving other + # clients. + def private=(value) + value = value ? true : nil + self.cache_control = cache_control. + merge('public' => !value, 'private' => value) + end + + # Indicates that the cache must not serve a stale response in any + # circumstance without first revalidating with the origin. When present, + # the TTL of the response should not be overriden to be greater than the + # value provided by the origin. + def must_revalidate? + cache_control['must-revalidate'] || + cache_control['proxy-revalidate'] + end + # The date, as specified by the Date header. When no Date header is present, # set the Date header to Time.now and return. def date - @date ||= - if date = headers['Date'] - Time.httpdate(date) - else - headers['Date'] = now.httpdate unless headers.frozen? - now - end + if date = headers['Date'] + Time.httpdate(date) + else + headers['Date'] = now.httpdate unless headers.frozen? + now + end end # The age of the response. def age [(now - date).to_i, 0].max end # The number of seconds after the time specified in the response's Date # header when the the response should no longer be considered fresh. First - # check for a Cache-Control max-age value, and fall back on an expires - # header; return nil when no maximum age can be established. + # check for a s-maxage directive, then a max-age directive, and then fall + # back on an expires header; return nil when no maximum age can be + # established. def max_age - if age = cache_control['max-age'] + if age = (cache_control['s-maxage'] || cache_control['max-age']) age.to_i elsif headers['Expires'] Time.httpdate(headers['Expires']) - date end end - # Sets the number of seconds after which the response should no longer - # be considered fresh. This sets the Cache-Control max-age value. + # The number of seconds after which the response should no longer + # be considered fresh. Sets the Cache-Control max-age directive. def max_age=(value) self.cache_control = cache_control.merge('max-age' => value.to_s) end + # Like #max_age= but sets the s-maxage directive, which applies only + # to shared caches. + def shared_max_age=(value) + self.cache_control = cache_control.merge('s-maxage' => value.to_s) + end + # The Time when the response should be considered stale. With a # Cache-Control/max-age value is present, this is calculated by adding the # number of seconds specified to the responses #date value. Falls back to # the time specified in the Expires header or returns nil if neither is # present. def expires_at - if max_age = cache_control['max-age'] + if max_age = (cache_control['s-maxage'] || cache_control['max-age']) date + max_age.to_i elsif time = headers['Expires'] Time.httpdate(time) end end @@ -189,13 +241,19 @@ # revalidating with the origin. def ttl max_age - age if max_age end - # Set the response's time-to-live to the specified number of seconds. This - # adjusts the Cache-Control/max-age value. + # Set the response's time-to-live for shared caches to the specified number + # of seconds. This adjusts the Cache-Control/s-maxage directive. def ttl=(seconds) + self.shared_max_age = age + seconds + end + + # Set the response's time-to-live for private/client caches. This adjusts + # the Cache-Control/max-age directive. + def client_ttl=(seconds) self.max_age = age + seconds end # The String value of the Last-Modified header exactly as it appears # in the response (i.e., no date parsing / conversion is performed). @@ -208,11 +266,41 @@ # Last-Modified header. def last_modified_at?(time_value) time_value && last_modified == time_value end - # The literal value of the Vary header, or nil when no Vary header is - # present. + # Determine if response's ETag matches the etag value provided. Return + # false when either value is nil. + def etag_matches?(etag) + etag && self.etag == etag + end + + # Headers that MUST NOT be included with 304 Not Modified responses. + # + # http://tools.ietf.org/html/rfc2616#section-10.3.5 + NOT_MODIFIED_OMIT_HEADERS = %w[ + Allow + Content-Encoding + Content-Language + Content-Length + Content-Md5 + Content-Type + Last-Modified + ].to_set + + # Modify the response so that it conforms to the rules defined for + # '304 Not Modified'. This sets the status, removes the body, and + # discards any headers that MUST NOT be included in 304 responses. + # + # http://tools.ietf.org/html/rfc2616#section-10.3.5 + def not_modified! + self.status = 304 + self.body = [] + NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) } + nil + end + + # The literal value of the Vary header, or nil when no header is present. def vary headers['Vary'] end # Does the response include a Vary header?