rack-cache 0.3.0 → 0.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/CHANGES +43 -0
  2. data/README +18 -9
  3. data/Rakefile +1 -14
  4. data/TODO +13 -14
  5. data/doc/configuration.markdown +7 -153
  6. data/doc/faq.markdown +8 -0
  7. data/doc/index.markdown +7 -9
  8. data/example/sinatra/app.rb +25 -0
  9. data/example/sinatra/views/index.erb +44 -0
  10. data/lib/rack/cache.rb +5 -11
  11. data/lib/rack/cache/cachecontrol.rb +193 -0
  12. data/lib/rack/cache/context.rb +190 -52
  13. data/lib/rack/cache/entitystore.rb +10 -4
  14. data/lib/rack/cache/key.rb +52 -0
  15. data/lib/rack/cache/metastore.rb +52 -16
  16. data/lib/rack/cache/options.rb +60 -39
  17. data/lib/rack/cache/request.rb +11 -15
  18. data/lib/rack/cache/response.rb +221 -30
  19. data/lib/rack/cache/storage.rb +1 -2
  20. data/rack-cache.gemspec +9 -15
  21. data/test/cache_test.rb +9 -6
  22. data/test/cachecontrol_test.rb +139 -0
  23. data/test/context_test.rb +251 -169
  24. data/test/entitystore_test.rb +12 -11
  25. data/test/key_test.rb +50 -0
  26. data/test/metastore_test.rb +57 -14
  27. data/test/options_test.rb +11 -0
  28. data/test/request_test.rb +19 -0
  29. data/test/response_test.rb +164 -23
  30. data/test/spec_setup.rb +7 -0
  31. metadata +12 -20
  32. data/doc/events.dot +0 -27
  33. data/lib/rack/cache/config.rb +0 -65
  34. data/lib/rack/cache/config/busters.rb +0 -16
  35. data/lib/rack/cache/config/default.rb +0 -133
  36. data/lib/rack/cache/config/no-cache.rb +0 -13
  37. data/lib/rack/cache/core.rb +0 -299
  38. data/lib/rack/cache/headers.rb +0 -325
  39. data/lib/rack/utils/environment_headers.rb +0 -78
  40. data/test/config_test.rb +0 -66
  41. data/test/core_test.rb +0 -84
  42. data/test/environment_headers_test.rb +0 -69
  43. data/test/headers_test.rb +0 -298
  44. data/test/logging_test.rb +0 -45
@@ -1,6 +1,7 @@
1
1
  require 'digest/sha1'
2
2
 
3
3
  module Rack::Cache
4
+
4
5
  # Entity stores are used to cache response bodies across requests. All
5
6
  # Implementations are required to calculate a SHA checksum of the data written
6
7
  # which becomes the response body's key.
@@ -13,7 +14,7 @@ module Rack::Cache
13
14
  def slurp(body)
14
15
  digest, size = Digest::SHA1.new, 0
15
16
  body.each do |part|
16
- size += part.length
17
+ size += bytesize(part)
17
18
  digest << part
18
19
  yield part
19
20
  end
@@ -21,7 +22,13 @@ module Rack::Cache
21
22
  [ digest.hexdigest, size ]
22
23
  end
23
24
 
24
- private :slurp
25
+ if ''.respond_to?(:bytesize)
26
+ def bytesize(string); string.bytesize; end
27
+ else
28
+ def bytesize(string); string.size; end
29
+ end
30
+
31
+ private :slurp, :bytesize
25
32
 
26
33
 
27
34
  # Stores entity bodies on the heap using a Hash object.
@@ -90,7 +97,7 @@ module Rack::Cache
90
97
  end
91
98
 
92
99
  def read(key)
93
- File.read(body_path(key))
100
+ File.open(body_path(key), 'rb') { |f| f.read }
94
101
  rescue Errno::ENOENT
95
102
  nil
96
103
  end
@@ -241,7 +248,6 @@ module Rack::Cache
241
248
 
242
249
  MEMCACHE = MemCache
243
250
  MEMCACHED = MemCache
244
-
245
251
  end
246
252
 
247
253
  end
@@ -0,0 +1,52 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack::Cache
4
+ class Key
5
+ include Rack::Utils
6
+
7
+ # Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
8
+ # opens the door for cache key generators to just be blocks.
9
+ def self.call(request)
10
+ new(request).generate
11
+ end
12
+
13
+ def initialize(request)
14
+ @request = request
15
+ end
16
+
17
+ # Generate a normalized cache key for the request.
18
+ def generate
19
+ parts = []
20
+ parts << @request.scheme << "://"
21
+ parts << @request.host
22
+
23
+ if @request.scheme == "https" && @request.port != 443 ||
24
+ @request.scheme == "http" && @request.port != 80
25
+ parts << ":" << @request.port.to_s
26
+ end
27
+
28
+ parts << @request.script_name
29
+ parts << @request.path_info
30
+
31
+ if qs = query_string
32
+ parts << "?"
33
+ parts << qs
34
+ end
35
+
36
+ parts.join
37
+ end
38
+
39
+ private
40
+ # Build a normalized query string by alphabetizing all keys/values
41
+ # and applying consistent escaping.
42
+ def query_string
43
+ return nil if @request.query_string.nil?
44
+
45
+ @request.query_string.split(/[&;] */n).
46
+ map { |p| unescape(p).split('=', 2) }.
47
+ sort.
48
+ map { |k,v| "#{escape(k)}=#{escape(v)}" }.
49
+ join('&')
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
- require 'rack'
2
1
  require 'fileutils'
3
2
  require 'digest/sha1'
3
+ require 'rack/utils'
4
4
 
5
5
  module Rack::Cache
6
6
 
@@ -25,7 +25,8 @@ module Rack::Cache
25
25
  # Rack::Cache::Response object if the cache hits or nil if no cache entry
26
26
  # was found.
27
27
  def lookup(request, entity_store)
28
- entries = read(request.fullpath)
28
+ key = cache_key(request)
29
+ entries = read(key)
29
30
 
30
31
  # bail out if we have nothing cached
31
32
  return nil if entries.empty?
@@ -37,9 +38,7 @@ module Rack::Cache
37
38
 
38
39
  req, res = match
39
40
  if body = entity_store.open(res['X-Content-Digest'])
40
- response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
41
- response.activate!
42
- response
41
+ restore_response(res, body)
43
42
  else
44
43
  # TODO the metastore referenced an entity that doesn't exist in
45
44
  # the entitystore. we definitely want to return nil but we should
@@ -50,21 +49,17 @@ module Rack::Cache
50
49
  # Write a cache entry to the store under the given key. Existing
51
50
  # entries are read and any that match the response are removed.
52
51
  # This method calls #write with the new list of cache entries.
53
- #--
54
- # TODO canonicalize URL key
55
52
  def store(request, response, entity_store)
56
- key = request.fullpath
53
+ key = cache_key(request)
57
54
  stored_env = persist_request(request)
58
55
 
59
56
  # write the response body to the entity store if this is the
60
57
  # original response.
61
- response['X-Status'] = response.status.to_s
62
- if response['X-Content-Digest'].nil?
58
+ if response.headers['X-Content-Digest'].nil?
63
59
  digest, size = entity_store.write(response.body)
64
- response['X-Content-Digest'] = digest
65
- response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
60
+ response.headers['X-Content-Digest'] = digest
61
+ response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
66
62
  response.body = entity_store.open(digest)
67
- response.activate!
68
63
  end
69
64
 
70
65
  # read existing cache entries, remove non-varying, and add this one to
@@ -75,11 +70,41 @@ module Rack::Cache
75
70
  (vary == res['Vary']) &&
76
71
  requests_match?(vary, env, stored_env)
77
72
  end
78
- entries.unshift [stored_env, {}.update(response.headers)]
73
+
74
+ headers = persist_response(response)
75
+ headers.delete 'Age'
76
+
77
+ entries.unshift [stored_env, headers]
79
78
  write key, entries
79
+ key
80
+ end
81
+
82
+ # Generate a cache key for the request.
83
+ def cache_key(request)
84
+ keygen = request.env['rack-cache.cache_key'] || Key
85
+ keygen.call(request)
86
+ end
87
+
88
+ # Invalidate all cache entries that match the request.
89
+ def invalidate(request, entity_store)
90
+ modified = false
91
+ key = cache_key(request)
92
+ entries =
93
+ read(key).map do |req, res|
94
+ response = restore_response(res)
95
+ if response.fresh?
96
+ response.expire!
97
+ modified = true
98
+ [req, persist_response(response)]
99
+ else
100
+ [req, res]
101
+ end
102
+ end
103
+ write key, entries if modified
80
104
  end
81
105
 
82
106
  private
107
+
83
108
  # Extract the environment Hash from +request+ while making any
84
109
  # necessary modifications in preparation for persistence. The Hash
85
110
  # returned must be marshalable.
@@ -89,6 +114,19 @@ module Rack::Cache
89
114
  env
90
115
  end
91
116
 
117
+ # Converts a stored response hash into a Response object. The caller
118
+ # is responsible for loading and passing the body if needed.
119
+ def restore_response(hash, body=nil)
120
+ status = hash.delete('X-Status').to_i
121
+ Rack::Cache::Response.new(status, hash, body)
122
+ end
123
+
124
+ def persist_response(response)
125
+ hash = response.headers.to_hash
126
+ hash['X-Status'] = response.status.to_s
127
+ hash
128
+ end
129
+
92
130
  # Determine whether the two environment hashes are non-varying based on
93
131
  # the vary response header value provided.
94
132
  def requests_match?(vary, env1, env2)
@@ -122,7 +160,6 @@ module Rack::Cache
122
160
  end
123
161
 
124
162
  private
125
-
126
163
  # Generate a SHA1 hex digest for the specified string. This is a
127
164
  # simple utility method for meta store implementations.
128
165
  def hexdigest(data)
@@ -130,7 +167,6 @@ module Rack::Cache
130
167
  end
131
168
 
132
169
  public
133
-
134
170
  # Concrete MetaStore implementation that uses a simple Hash to store
135
171
  # request/response pairs on the heap.
136
172
  class Heap < MetaStore
@@ -1,26 +1,28 @@
1
- require 'rack'
1
+ require 'rack/cache/key'
2
2
  require 'rack/cache/storage'
3
3
 
4
4
  module Rack::Cache
5
+
5
6
  # Configuration options and utility methods for option access. Rack::Cache
6
7
  # uses the Rack Environment to store option values. All options documented
7
8
  # below are stored in the Rack Environment as "rack-cache.<option>", where
8
9
  # <option> is the option name.
9
- #
10
- # The #set method can be used within an event or a top-level configuration
11
- # block to configure a option values. When #set is called at the top-level,
12
- # the value applies to all requests; when called from within an event, the
13
- # values applies only to the request being processed.
14
-
15
10
  module Options
16
- class << self
17
- private
18
- def option_accessor(key)
19
- define_method(key) { || read_option(key) }
20
- define_method("#{key}=") { |value| write_option(key, value) }
21
- define_method("#{key}?") { || !! read_option(key) }
11
+ def self.option_accessor(key)
12
+ name = option_name(key)
13
+ define_method(key) { || options[name] }
14
+ define_method("#{key}=") { |value| options[name] = value }
15
+ define_method("#{key}?") { || !! options[name] }
16
+ end
17
+
18
+ def option_name(key)
19
+ case key
20
+ when Symbol ; "rack-cache.#{key}"
21
+ when String ; key
22
+ else raise ArgumentError
22
23
  end
23
24
  end
25
+ module_function :option_name
24
26
 
25
27
  # Enable verbose trace logging. This option is currently enabled by
26
28
  # default but is likely to be disabled in a future release.
@@ -44,9 +46,22 @@ module Rack::Cache
44
46
  # recommended.
45
47
  option_accessor :metastore
46
48
 
47
- # A URI specifying the entity-store implement that should be used to store
48
- # response bodies. See the metastore option for information on supported URI
49
- # schemes.
49
+ # A custom cache key generator, which can be anything that responds to :call.
50
+ # By default, this is the Rack::Cache::Key class, but you can implement your
51
+ # own generator. A cache key generator gets passed a request and generates the
52
+ # appropriate cache key.
53
+ #
54
+ # In addition to setting the generator to an object, you can just pass a block
55
+ # instead, which will act as the cache key generator:
56
+ #
57
+ # set :cache_key do |request|
58
+ # request.fullpath.replace(/\//, '-')
59
+ # end
60
+ option_accessor :cache_key
61
+
62
+ # A URI specifying the entity-store implementation that should be used to
63
+ # store response bodies. See the metastore option for information on
64
+ # supported URI schemes.
50
65
  #
51
66
  # If no entity store is specified the 'heap:/' store is assumed. This
52
67
  # implementation has significant draw-backs so explicit configuration is
@@ -70,6 +85,16 @@ module Rack::Cache
70
85
  # Default: ['Authorization', 'Cookie']
71
86
  option_accessor :private_headers
72
87
 
88
+ # Specifies whether the client can force a cache reload by including a
89
+ # Cache-Control "no-cache" directive in the request. This is enabled by
90
+ # default for compliance with RFC 2616.
91
+ option_accessor :allow_reload
92
+
93
+ # Specifies whether the client can force a cache revalidate by including
94
+ # a Cache-Control "max-age=0" directive in the request. This is enabled by
95
+ # default for compliance with RFC 2616.
96
+ option_accessor :allow_revalidate
97
+
73
98
  # The underlying options Hash. During initialization (or outside of a
74
99
  # request), this is a default values Hash. During a request, this is the
75
100
  # Rack environment Hash. The default values Hash is merged in underneath
@@ -88,8 +113,10 @@ module Rack::Cache
88
113
  # exactly as specified. The +option+ argument may also be a Hash in
89
114
  # which case each key/value pair is merged into the environment as if
90
115
  # the #set method were called on each.
91
- def set(option, value=self)
92
- if value == self
116
+ def set(option, value=self, &block)
117
+ if block_given?
118
+ write_option option, block
119
+ elsif value == self
93
120
  self.options = option.to_hash
94
121
  else
95
122
  write_option option, value
@@ -97,6 +124,21 @@ module Rack::Cache
97
124
  end
98
125
 
99
126
  private
127
+ def initialize_options(options={})
128
+ @default_options = {
129
+ 'rack-cache.cache_key' => Key,
130
+ 'rack-cache.verbose' => true,
131
+ 'rack-cache.storage' => Rack::Cache::Storage.instance,
132
+ 'rack-cache.metastore' => 'heap:/',
133
+ 'rack-cache.entitystore' => 'heap:/',
134
+ 'rack-cache.default_ttl' => 0,
135
+ 'rack-cache.private_headers' => ['Authorization', 'Cookie'],
136
+ 'rack-cache.allow_reload' => true,
137
+ 'rack-cache.allow_revalidate' => true
138
+ }
139
+ self.options = options
140
+ end
141
+
100
142
  def read_option(key)
101
143
  options[option_name(key)]
102
144
  end
@@ -104,26 +146,5 @@ module Rack::Cache
104
146
  def write_option(key, value)
105
147
  options[option_name(key)] = value
106
148
  end
107
-
108
- def option_name(key)
109
- case key
110
- when Symbol ; "rack-cache.#{key}"
111
- when String ; key
112
- else raise ArgumentError
113
- end
114
- end
115
-
116
- private
117
- def initialize_options(options={})
118
- @default_options = {
119
- 'rack-cache.verbose' => true,
120
- 'rack-cache.storage' => Rack::Cache::Storage.instance,
121
- 'rack-cache.metastore' => 'heap:/',
122
- 'rack-cache.entitystore' => 'heap:/',
123
- 'rack-cache.default_ttl' => 0,
124
- 'rack-cache.private_headers' => ['Authorization', 'Cookie']
125
- }
126
- self.options = options
127
- end
128
149
  end
129
150
  end
@@ -1,19 +1,15 @@
1
1
  require 'rack/request'
2
- require 'rack/cache/headers'
3
- require 'rack/utils/environment_headers'
2
+ require 'rack/cache/cachecontrol'
4
3
 
5
4
  module Rack::Cache
5
+
6
6
  # Provides access to the HTTP request. The +request+ and +original_request+
7
7
  # objects exposed by the Core caching engine are instances of this class.
8
8
  #
9
9
  # Request objects respond to a variety of convenience methods, including
10
10
  # everything defined by Rack::Request as well as the Headers and
11
11
  # RequestHeaders modules.
12
-
13
12
  class Request < Rack::Request
14
- include Rack::Cache::Headers
15
- include Rack::Cache::RequestHeaders
16
-
17
13
  # The HTTP request method. This is the standard implementation of this
18
14
  # method but is respecified here due to libraries that attempt to modify
19
15
  # the behavior to respect POST tunnel method specifiers. We always want
@@ -22,16 +18,16 @@ module Rack::Cache
22
18
  @env['REQUEST_METHOD']
23
19
  end
24
20
 
25
- # Determine if the request's method matches any of the values
26
- # provided:
27
- # if request.request_method?('GET', 'POST')
28
- # ...
29
- # end
30
- def request_method?(*methods)
31
- method = request_method
32
- methods.any? { |test| test.to_s.upcase == method }
21
+ # A CacheControl instance based on the request's Cache-Control header.
22
+ def cache_control
23
+ @cache_control ||= CacheControl.new(env['HTTP_CACHE_CONTROL'])
33
24
  end
34
25
 
35
- alias_method :method?, :request_method?
26
+ # True when the Cache-Control/no-cache directive is present or the
27
+ # Pragma header is set to no-cache.
28
+ def no_cache?
29
+ cache_control['no-cache'] ||
30
+ env['HTTP_PRAGMA'] == 'no-cache'
31
+ end
36
32
  end
37
33
  end
@@ -1,7 +1,11 @@
1
+ require 'time'
1
2
  require 'set'
2
- require 'rack/cache/headers'
3
+ require 'rack/response'
4
+ require 'rack/utils'
5
+ require 'rack/cache/cachecontrol'
3
6
 
4
7
  module Rack::Cache
8
+
5
9
  # Provides access to the response generated by the downstream application. The
6
10
  # +response+, +original_response+, and +entry+ objects exposed by the Core
7
11
  # caching engine are instances of this class.
@@ -14,21 +18,14 @@ module Rack::Cache
14
18
  # not perform many of the same initialization and finalization tasks. For
15
19
  # example, the body is not slurped during initialization and there are no
16
20
  # facilities for generating response output.
17
-
18
21
  class Response
19
22
  include Rack::Response::Helpers
20
- include Rack::Cache::Headers
21
- include Rack::Cache::ResponseHeaders
22
23
 
23
- # The response's status code (integer).
24
- attr_accessor :status
24
+ # Rack response tuple accessors.
25
+ attr_accessor :status, :headers, :body
25
26
 
26
- # The response body. See the Rack spec for information on the behavior
27
- # required by this object.
28
- attr_accessor :body
29
-
30
- # The response headers.
31
- attr_reader :headers
27
+ # The time when the Response object was instantiated.
28
+ attr_reader :now
32
29
 
33
30
  # Create a Response instance given the response status code, header hash,
34
31
  # and body.
@@ -37,7 +34,7 @@ module Rack::Cache
37
34
  @headers = Rack::Utils::HeaderHash.new(headers)
38
35
  @body = body
39
36
  @now = Time.now
40
- @headers['Date'] ||= now.httpdate
37
+ @headers['Date'] ||= @now.httpdate
41
38
  end
42
39
 
43
40
  def initialize_copy(other)
@@ -45,32 +42,226 @@ module Rack::Cache
45
42
  @headers = other.headers.dup
46
43
  end
47
44
 
48
- # Return the value of the named response header.
49
- def [](header_name)
50
- headers[header_name]
45
+ # Return the status, headers, and body in a three-tuple.
46
+ def to_a
47
+ [status, headers.to_hash, body]
51
48
  end
52
49
 
53
- # Set a response header value.
54
- def []=(header_name, header_value)
55
- headers[header_name] = header_value
50
+ # Status codes of responses that MAY be stored by a cache or used in reply
51
+ # to a subsequent request.
52
+ #
53
+ # http://tools.ietf.org/html/rfc2616#section-13.4
54
+ CACHEABLE_RESPONSE_CODES = [
55
+ 200, # OK
56
+ 203, # Non-Authoritative Information
57
+ 300, # Multiple Choices
58
+ 301, # Moved Permanently
59
+ 302, # Found
60
+ 404, # Not Found
61
+ 410 # Gone
62
+ ].to_set
63
+
64
+ # A Hash of name=value pairs that correspond to the Cache-Control header.
65
+ # Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
66
+ # of true. This method always returns a Hash, empty if no Cache-Control
67
+ # header is present.
68
+ def cache_control
69
+ @cache_control ||= CacheControl.new(headers['Cache-Control'])
56
70
  end
57
71
 
58
- # Called immediately after an object is loaded from the cache.
59
- def activate!
60
- headers['Age'] = age.to_i.to_s
72
+ # Set the Cache-Control header to the values specified by the Hash. See
73
+ # the #cache_control method for information on expected Hash structure.
74
+ def cache_control=(value)
75
+ if value.respond_to? :to_hash
76
+ cache_control.clear
77
+ cache_control.merge!(value)
78
+ value = cache_control.to_s
79
+ end
80
+
81
+ if value.nil? || value.empty?
82
+ headers.delete('Cache-Control')
83
+ else
84
+ headers['Cache-Control'] = value
85
+ end
61
86
  end
62
87
 
63
- # Return the status, headers, and body in a three-tuple.
64
- def to_a
65
- [status, headers.to_hash, body]
88
+ # Determine if the response is "fresh". Fresh responses may be served from
89
+ # cache without any interaction with the origin. A response is considered
90
+ # fresh when it includes a Cache-Control/max-age indicator or Expiration
91
+ # header and the calculated age is less than the freshness lifetime.
92
+ def fresh?
93
+ ttl && ttl > 0
66
94
  end
67
95
 
68
- # Freezes
69
- def freeze
70
- @headers.freeze
71
- super
96
+ # Determine if the response is worth caching under any circumstance. Responses
97
+ # marked "private" with an explicit Cache-Control directive are considered
98
+ # uncacheable
99
+ #
100
+ # Responses with neither a freshness lifetime (Expires, max-age) nor cache
101
+ # validator (Last-Modified, ETag) are considered uncacheable.
102
+ def cacheable?
103
+ return false unless CACHEABLE_RESPONSE_CODES.include?(status)
104
+ return false if cache_control.no_store? || cache_control.private?
105
+ validateable? || fresh?
72
106
  end
73
107
 
74
- end
108
+ # Determine if the response includes headers that can be used to validate
109
+ # the response with the origin using a conditional GET request.
110
+ def validateable?
111
+ headers.key?('Last-Modified') || headers.key?('ETag')
112
+ end
113
+
114
+ # Mark the response "private", making it ineligible for serving other
115
+ # clients.
116
+ def private=(value)
117
+ value = value ? true : nil
118
+ self.cache_control = cache_control.
119
+ merge('public' => !value, 'private' => value)
120
+ end
121
+
122
+ # Indicates that the cache must not serve a stale response in any
123
+ # circumstance without first revalidating with the origin. When present,
124
+ # the TTL of the response should not be overriden to be greater than the
125
+ # value provided by the origin.
126
+ def must_revalidate?
127
+ cache_control.must_revalidate || cache_control.proxy_revalidate
128
+ end
129
+
130
+ # Mark the response stale by setting the Age header to be equal to the
131
+ # maximum age of the response.
132
+ def expire!
133
+ headers['Age'] = max_age.to_s if fresh?
134
+ end
135
+
136
+ # The date, as specified by the Date header. When no Date header is present,
137
+ # set the Date header to Time.now and return.
138
+ def date
139
+ if date = headers['Date']
140
+ Time.httpdate(date)
141
+ else
142
+ headers['Date'] = now.httpdate unless headers.frozen?
143
+ now
144
+ end
145
+ end
146
+
147
+ # The age of the response.
148
+ def age
149
+ (headers['Age'] || [(now - date).to_i, 0].max).to_i
150
+ end
151
+
152
+ # The number of seconds after the time specified in the response's Date
153
+ # header when the the response should no longer be considered fresh. First
154
+ # check for a s-maxage directive, then a max-age directive, and then fall
155
+ # back on an expires header; return nil when no maximum age can be
156
+ # established.
157
+ def max_age
158
+ cache_control.shared_max_age ||
159
+ cache_control.max_age ||
160
+ (expires && (expires - date))
161
+ end
162
+
163
+ # The value of the Expires header as a Time object.
164
+ def expires
165
+ headers['Expires'] && Time.httpdate(headers['Expires'])
166
+ end
75
167
 
168
+ # The number of seconds after which the response should no longer
169
+ # be considered fresh. Sets the Cache-Control max-age directive.
170
+ def max_age=(value)
171
+ self.cache_control = cache_control.merge('max-age' => value.to_s)
172
+ end
173
+
174
+ # Like #max_age= but sets the s-maxage directive, which applies only
175
+ # to shared caches.
176
+ def shared_max_age=(value)
177
+ self.cache_control = cache_control.merge('s-maxage' => value.to_s)
178
+ end
179
+
180
+ # The response's time-to-live in seconds, or nil when no freshness
181
+ # information is present in the response. When the responses #ttl
182
+ # is <= 0, the response may not be served from cache without first
183
+ # revalidating with the origin.
184
+ def ttl
185
+ max_age - age if max_age
186
+ end
187
+
188
+ # Set the response's time-to-live for shared caches to the specified number
189
+ # of seconds. This adjusts the Cache-Control/s-maxage directive.
190
+ def ttl=(seconds)
191
+ self.shared_max_age = age + seconds
192
+ end
193
+
194
+ # Set the response's time-to-live for private/client caches. This adjusts
195
+ # the Cache-Control/max-age directive.
196
+ def client_ttl=(seconds)
197
+ self.max_age = age + seconds
198
+ end
199
+
200
+ # The String value of the Last-Modified header exactly as it appears
201
+ # in the response (i.e., no date parsing / conversion is performed).
202
+ def last_modified
203
+ headers['Last-Modified']
204
+ end
205
+
206
+ # The literal value of ETag HTTP header or nil if no ETag is specified.
207
+ def etag
208
+ headers['ETag']
209
+ end
210
+
211
+ # Determine if the response was last modified at the time provided.
212
+ # time_value is the exact string provided in an origin response's
213
+ # Last-Modified header.
214
+ def last_modified_at?(time_value)
215
+ time_value && last_modified == time_value
216
+ end
217
+
218
+ # Determine if response's ETag matches the etag value provided. Return
219
+ # false when either value is nil.
220
+ def etag_matches?(etag)
221
+ etag && self.etag == etag
222
+ end
223
+
224
+ # Headers that MUST NOT be included with 304 Not Modified responses.
225
+ #
226
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
227
+ NOT_MODIFIED_OMIT_HEADERS = %w[
228
+ Allow
229
+ Content-Encoding
230
+ Content-Language
231
+ Content-Length
232
+ Content-MD5
233
+ Content-Type
234
+ Last-Modified
235
+ ].to_set
236
+
237
+ # Modify the response so that it conforms to the rules defined for
238
+ # '304 Not Modified'. This sets the status, removes the body, and
239
+ # discards any headers that MUST NOT be included in 304 responses.
240
+ #
241
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
242
+ def not_modified!
243
+ self.status = 304
244
+ self.body = []
245
+ NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
246
+ nil
247
+ end
248
+
249
+ # The literal value of the Vary header, or nil when no header is present.
250
+ def vary
251
+ headers['Vary']
252
+ end
253
+
254
+ # Does the response include a Vary header?
255
+ def vary?
256
+ ! vary.nil?
257
+ end
258
+
259
+ # An array of header names given in the Vary header or an empty
260
+ # array when no Vary header is present.
261
+ def vary_header_names
262
+ return [] unless vary = headers['Vary']
263
+ vary.split(/[\s,]+/)
264
+ end
265
+
266
+ end
76
267
  end