rack-cache 0.3.0 → 0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +43 -0
- data/README +18 -9
- data/Rakefile +1 -14
- data/TODO +13 -14
- data/doc/configuration.markdown +7 -153
- data/doc/faq.markdown +8 -0
- data/doc/index.markdown +7 -9
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +5 -11
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +190 -52
- data/lib/rack/cache/entitystore.rb +10 -4
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +52 -16
- data/lib/rack/cache/options.rb +60 -39
- data/lib/rack/cache/request.rb +11 -15
- data/lib/rack/cache/response.rb +221 -30
- data/lib/rack/cache/storage.rb +1 -2
- data/rack-cache.gemspec +9 -15
- data/test/cache_test.rb +9 -6
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +251 -169
- data/test/entitystore_test.rb +12 -11
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +57 -14
- data/test/options_test.rb +11 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +164 -23
- data/test/spec_setup.rb +7 -0
- metadata +12 -20
- data/doc/events.dot +0 -27
- data/lib/rack/cache/config.rb +0 -65
- data/lib/rack/cache/config/busters.rb +0 -16
- data/lib/rack/cache/config/default.rb +0 -133
- data/lib/rack/cache/config/no-cache.rb +0 -13
- data/lib/rack/cache/core.rb +0 -299
- data/lib/rack/cache/headers.rb +0 -325
- data/lib/rack/utils/environment_headers.rb +0 -78
- data/test/config_test.rb +0 -66
- data/test/core_test.rb +0 -84
- data/test/environment_headers_test.rb +0 -69
- data/test/headers_test.rb +0 -298
- 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
|
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
|
-
|
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.
|
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
|
data/lib/rack/cache/metastore.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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-
|
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
|
-
|
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
|
data/lib/rack/cache/options.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
48
|
-
#
|
49
|
-
#
|
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
|
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
|
data/lib/rack/cache/request.rb
CHANGED
@@ -1,19 +1,15 @@
|
|
1
1
|
require 'rack/request'
|
2
|
-
require 'rack/cache/
|
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
|
-
#
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
data/lib/rack/cache/response.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
require 'time'
|
1
2
|
require 'set'
|
2
|
-
require 'rack/
|
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
|
-
#
|
24
|
-
attr_accessor :status
|
24
|
+
# Rack response tuple accessors.
|
25
|
+
attr_accessor :status, :headers, :body
|
25
26
|
|
26
|
-
# The
|
27
|
-
|
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
|
49
|
-
def
|
50
|
-
headers
|
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
|
-
#
|
54
|
-
|
55
|
-
|
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
|
-
#
|
59
|
-
|
60
|
-
|
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
|
-
#
|
64
|
-
|
65
|
-
|
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
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|