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?