require 'time' require 'set' require 'rack/response' require 'rack/utils' require 'rack/cache/cachecontrol' module Rack::Cache # Provides access to the response generated by the downstream application. The # +response+, +original_response+, and +entry+ objects exposed by the Core # caching engine are instances of this class. # # Response objects respond to a variety of convenience methods, including # those defined in Rack::Response::Helpers, Rack::Cache::Headers, # and Rack::Cache::ResponseHeaders. # # Note that Rack::Cache::Response is not a subclass of Rack::Response and does # not perform many of the same initialization and finalization tasks. For # example, the body is not slurped during initialization and there are no # facilities for generating response output. class Response include Rack::Response::Helpers # Rack response tuple accessors. attr_accessor :status, :headers, :body # The time when the Response object was instantiated. attr_reader :now # Create a Response instance given the response status code, header hash, # and body. def initialize(status, headers, body) @status = status.to_i @headers = Rack::Utils::HeaderHash.new(headers) @body = body @now = Time.now @headers['Date'] ||= @now.httpdate end def initialize_copy(other) super @headers = other.headers.dup end # Return the status, headers, and body in a three-tuple. def to_a [status, headers.to_hash, body] end # 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 # A Hash of name=value pairs that correspond to the Cache-Control header. # 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 ||= CacheControl.new(headers['Cache-Control']) end # Set the Cache-Control header to the values specified by the Hash. See # the #cache_control method for information on expected Hash structure. def cache_control=(value) if value.respond_to? :to_hash cache_control.clear cache_control.merge!(value) value = cache_control.to_s end if value.nil? || value.empty? headers.delete('Cache-Control') else headers['Cache-Control'] = value end end # 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. def fresh? ttl && ttl > 0 end # Determine if the response is worth caching under any circumstance. Responses # marked "private" with an explicit Cache-Control directive are considered # uncacheable # # 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 cache_control.no_store? || cache_control.private? validateable? || fresh? 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? headers.key?('Last-Modified') || headers.key?('ETag') 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 # Mark the response stale by setting the Age header to be equal to the # maximum age of the response. def expire! headers['Age'] = max_age.to_s if fresh? 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 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 (headers['Age'] || [(now - date).to_i, 0].max).to_i 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 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 cache_control.shared_max_age || cache_control.max_age || (expires && (expires - date)) end # The value of the Expires header as a Time object. def expires headers['Expires'] && Time.httpdate(headers['Expires']) end # 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 response's time-to-live in seconds, or nil when no freshness # information is present in the response. When the responses #ttl # is <= 0, the response may not be served from cache without first # revalidating with the origin. def ttl max_age - age if max_age end # 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). def last_modified headers['Last-Modified'] end # The literal value of ETag HTTP header or nil if no ETag is specified. def etag headers['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! body.close if body.respond_to?(:close) 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? def vary? ! vary.nil? end # An array of header names given in the Vary header or an empty # array when no Vary header is present. def vary_header_names return [] unless vary = headers['Vary'] vary.split(/[\s,]+/) end end end