rtomayko-rack-cache 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,23 +1,58 @@
1
- ## 0.3.0 / ...
2
-
3
- * Cache responses that have a Transfer-Encoding. The current implementation
4
- caches the encoded value and reproduces the message with the same
5
- Transfer-Encoding header on subsequent cache hits. This goes against
6
- RFC 2616, which says that intermediaries must fully decode responses with
7
- a Transfer-Encoding header before passing them on (though they may re-encode
8
- or apply a different encoding). This seems less relevant to Rack::Cache,
9
- however, since we're not a true "hop". We'll see how it goes and act
10
- accordingly.
1
+ ## 0.3.0 / December 2008
2
+
3
+ * Add support for public and private cache control directives. Responses
4
+ marked as explicitly public are cached even when the request includes
5
+ an Authorization or Cookie header. Responses marked as explicitly private
6
+ are considered uncacheable.
7
+
8
+ * Added a "private_headers" option that dictates which request headers
9
+ trigger default "private" cache control processing. By default, the
10
+ Cookie and Authorization headers are included. Headers may be added or
11
+ removed as necessary to change the default private logic.
12
+
13
+ * Adhere to must-revalidate/proxy-revalidate cache control directives by
14
+ not assigning the default_ttl to responses that don't include freshness
15
+ information. This should let us begin using default_ttl more liberally
16
+ since we can control it using the must-revalidate/proxy-revalidate directives.
17
+
18
+ * Use the s-maxage Cache-Control value in preference to max-age when
19
+ present. The ttl= method now sets the s-maxage value instead of max-age.
20
+ Code that used ttl= to control freshness at the client needs to change
21
+ to set the max-age directive explicitly.
22
+
23
+ * Enable support for X-Sendfile middleware by responding to #to_path on
24
+ bodies served from disk storage. Adding the Rack::Sendfile component
25
+ upstream from Rack::Cache will result in cached bodies being served
26
+ directly by the web server (instead of being read in Ruby).
27
+
28
+ * BUG: MetaStore hits but EntityStore misses. This would 500 previously; now
29
+ we detect it and act as if the MetaStore missed as well.
30
+
31
+ * Implement low level #purge method on all concrete entity store
32
+ classes -- removes the entity body corresponding to the SHA1 key
33
+ provided and returns nil.
34
+
35
+ * Basically sane handling of HEAD requests. A HEAD request is never passed
36
+ through to the backend except when transitioning with pass!. This means
37
+ that the cache responds to HEAD requests without invoking the backend at
38
+ all when the cached entry is fresh. When no cache entry exists, or the
39
+ cached entry is stale and can be validated, the backend is invoked with
40
+ a GET request and the HEAD is handled right before the response
41
+ is delivered upstream.
42
+
11
43
  * BUG: The Age response header was not being set properly when a stale
12
44
  entry was validated. This would result in Age values that exceeded
13
45
  the freshness lifetime in responses.
46
+
14
47
  * BUG: A cached entry in a heap meta store could be unintentionally
15
48
  modified by request processing since the cached objects were being
16
49
  returned directly. The result was typically missing/incorrect header
17
50
  values (e.g., missing Content-Type header). [dkubb]
51
+
18
52
  * BUG: 304 responses should not include entity headers (especially
19
53
  Content-Length). This is causing Safari/WebKit weirdness on 304
20
54
  responses.
55
+
21
56
  * BUG: The If-None-Match header was being ignored, causing the cache
22
57
  to send 200 responses to matching conditional GET requests.
23
58
 
data/README CHANGED
@@ -3,16 +3,14 @@ Rack::Cache
3
3
 
4
4
  Rack::Cache is suitable as a quick drop-in component to enable HTTP caching for
5
5
  Rack-based applications that produce freshness (Expires, Cache-Control) and/or
6
- validation (Last-Modified, ETag) information.
7
-
8
- We strive to implement those portions of RFC 2616 Section 13 relevant to gateway
9
- (i.e., "reverse proxy") cache scenarios with a system for specifying cache
10
- policy:
6
+ validation (Last-Modified, ETag) information:
11
7
 
12
8
  * Standards-based (RFC 2616)
13
9
  * Freshness/expiration based caching
14
- * Validation
10
+ * Validation (If-Modified-Since / If-None-Match)
15
11
  * Vary support
12
+ * Cache-Control: public, private, max-age, s-maxage, must-revalidate,
13
+ and proxy-revalidate.
16
14
  * Portable: 100% Ruby / works with any Rack-enabled framework
17
15
  * Configuration language for advanced caching policies
18
16
  * Disk, memcached, and heap memory storage backends
@@ -68,10 +66,17 @@ Assuming you've designed your backend application to take advantage of HTTP's
68
66
  caching features, no further code or configuration is required for basic
69
67
  caching.
70
68
 
71
- Documentation
72
- -------------
69
+ Links
70
+ -----
73
71
 
74
- http://tomayko.com/src/rack-cache/
72
+ Documentation:
73
+ http://tomayko.com/src/rack-cache/
74
+
75
+ Mailing List:
76
+ http://groups.google.com/group/rack-cache
77
+
78
+ GitHub:
79
+ http://github.com/rtomayko/rack-cache/
75
80
 
76
81
  License
77
82
  -------
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ end
21
21
  desc 'Run specs with unit test style output'
22
22
  task :test => FileList['test/*_test.rb'] do |t|
23
23
  suite = t.prerequisites
24
- sh "testrb -Ilib:test #{suite.join(' ')}", :verbose => false
24
+ sh "specrb -Ilib:test #{suite.join(' ')}", :verbose => false
25
25
  end
26
26
 
27
27
  desc 'Generate test coverage report'
@@ -72,10 +72,10 @@ desc 'Build markdown documentation files'
72
72
  task 'doc:markdown'
73
73
  FileList['doc/*.markdown'].each do |source|
74
74
  dest = "doc/#{File.basename(source, '.markdown')}.html"
75
- file dest => source do |f|
75
+ file dest => [source, 'doc/layout.html.erb'] do |f|
76
76
  puts "markdown: #{source} -> #{dest}" if verbose
77
- require 'erb'
78
- require 'rdiscount'
77
+ require 'erb' unless defined? ERB
78
+ require 'rdiscount' unless defined? RDiscount
79
79
  template = File.read(source)
80
80
  content = Markdown.new(ERB.new(template, 0, "%<>").result(binding), :smart).to_html
81
81
  title = content.match("<h1>(.*)</h1>")[1] rescue ''
@@ -87,10 +87,16 @@ FileList['doc/*.markdown'].each do |source|
87
87
  CLEAN.include dest
88
88
  end
89
89
 
90
+ desc 'Publish documentation'
90
91
  task 'doc:publish' => :doc do
91
92
  sh 'rsync -avz doc/ gus@tomayko.com:/src/rack-cache'
92
93
  end
93
94
 
95
+ desc 'Start the documentation development server (requires thin)'
96
+ task 'doc:server' do
97
+ sh 'cd doc && thin --rackup server.ru --port 3035 start'
98
+ end
99
+
94
100
  # PACKAGING =================================================================
95
101
 
96
102
  def package(ext='')
data/TODO CHANGED
@@ -1,42 +1,32 @@
1
- ## 0.3
1
+ ## 0.4
2
2
 
3
- - BUG: HEAD request on invalid entry caches zero-length response
4
- - BUG: meta store hits but entity misses
5
- - BUG: Response body written to cache each time validation succeeds
6
- (actually, I'm not positive whether this is happening or not but
7
- it looks like it is).
8
- - Are we doing HEAD properly?
9
3
  - liberal, conservative, sane caching configs
10
- - Sample app
11
- - busters.rb doc and tests
12
- - no-cache.rb doc and tests
4
+ - Sample apps: Rack, Rails, Sinatra, Merb, etc.
5
+ - busters.rb and no-cache.rb doc and tests
13
6
  - Canonicalized URL for cache key:
14
7
  - sorts params by key, then value
15
8
  - urlencodes /[^ A-Za-z0-9_.-]/ host, path, and param key/value
16
- - Support server-specific X-Sendfile (or similar) for delivering cached
17
- bodies (file entity stores only).
18
- - Sqlite3 (meta store)
9
+ - Custom cache keys
19
10
  - Cache invalidation on PUT, POST, DELETE.
20
11
  - Invalidate at the request URI; or, anything that's "near" the request URI.
21
12
  - Invalidate at the URI of the Location or Content-Location response header.
22
13
 
23
14
  ## Backlog
24
15
 
16
+ - Add missing Expires header if we have a max-age.
25
17
  - Purge/invalidate specific cache entries
26
18
  - Purge/invalidate everything
27
19
  - Maximum size of cached entity
28
20
  - Last-Modified factor: requests that have a Last-Modified header but no Expires
29
21
  header have a TTL assigned based on the last modified age of the response:
30
22
  TTL = (Age * Factor), or, 1h = (10h * 0.1)
31
- - I wonder if it would be possible to run in threaded mode but with an
32
- option to lock before making requests to the backend. The idea is to be
33
- able to serve requests from cache in separate threads. This should
23
+ - Run under multiple-threads with an option to lock before making requests
24
+ to the backend. The idea is to be able to serve requests from cache in
25
+ separate threads. This should probably be implemented as a separate
26
+ middleware component.
27
+ - Consider implementing ESI (http://www.w3.org/TR/esi-lang). This should
34
28
  probably be implemented as a separate middleware component.
29
+ - Sqlite3 (meta store)
35
30
  - stale-while-revalidate
36
31
  - Serve cached copies when down (see: stale-if-error) - e.g., database
37
32
  connection drops and the cache takes over what it can.
38
- - When a cache misses due to Vary, try to validate using the best match. Note
39
- that you can't do this with a weak validator, so only strong etags can be
40
- used.
41
- - Consider implementing ESI (http://www.w3.org/TR/esi-lang). This should
42
- probably be implemented as a separate middleware component.
@@ -115,6 +115,14 @@ If no entitystore is specified, the `heap:/` store is assumed. This
115
115
  implementation has significant draw-backs so explicit configuration is
116
116
  recommended.
117
117
 
118
+ ### `private_headers`
119
+
120
+ An array of request header names that cause the response to be treated with
121
+ private cache control semantics. The default value is `['Authorization', 'Cookie']`.
122
+ If any of these headers are present in the request, the response is considered
123
+ private and will not be cached _unless_ the response is explicitly marked public
124
+ (e.g., `Cache-Control: public`).
125
+
118
126
  <a id='machinery'></a>
119
127
 
120
128
  Configuration Machinery - Events and Transitions
data/doc/index.markdown CHANGED
@@ -2,10 +2,10 @@ __Rack::Cache__ is suitable as a quick drop-in component to enable HTTP caching
2
2
  for [Rack][]-based applications that produce freshness (`Expires`,
3
3
  `Cache-Control`) and/or validation (`Last-Modified`, `ETag`) information.
4
4
 
5
- * Standards-based ([RFC 2616][rfc] / [Section 13][s13]).
5
+ * Standards-based (see [RFC 2616][rfc] / [Section 13][s13]).
6
6
  * Freshness/expiration based caching
7
7
  * Validation
8
- * Vary Support
8
+ * Vary support
9
9
  * Portable: 100% Ruby / works with any [Rack][]-enabled framework.
10
10
  * [Configuration language][config] for advanced caching policies.
11
11
  * Disk, memcached, and heap memory [storage backends][storage].
@@ -47,29 +47,35 @@ simply `require` and `use` as follows:
47
47
 
48
48
  Assuming you've designed your backend application to take advantage of HTTP's
49
49
  caching features, no further code or configuration is required for basic
50
- caching. More sophisticated stuff is possible with [Rack::Cache's Configuration
51
- Language][config].
50
+ caching.
52
51
 
53
- Advanced Usage
54
- --------------
52
+ More
53
+ ----
55
54
 
56
- * [Configuration Language Documentation][config] - How to customize cache
55
+ * [Configuration Language Documentation][config] - how to customize cache
57
56
  policy using the simple event-based configuration system.
58
57
 
59
- * [Cache Storage Documentation][storage] - Detailed information on the various
58
+ * [Cache Storage Documentation][storage] - detailed information on the various
60
59
  storage implementations available in __Rack::Cache__ and how to choose the one
61
60
  that's best for your application.
62
61
 
63
- * [FAQ](./faq) - Frequently Asked Questions about __Rack::Cache__.
62
+ * [Things Caches Do][things] - an illustrated guide to how HTTP gateway
63
+ caches work with pointers to other useful resources on HTTP caching.
64
64
 
65
- * [GitHub Repository](http://github.com/rtomayko/rack-cache/) - Get your
65
+ * [GitHub Repository](http://github.com/rtomayko/rack-cache/) - get your
66
66
  fork on.
67
67
 
68
+ * [Mailing List](http://groups.google.com/group/rack-cache) - for hackers
69
+ and users (`rack-cache@groups.google.com`).
70
+
71
+ * [FAQ](./faq) - Frequently Asked Questions about __Rack::Cache__.
72
+
68
73
  * [RDoc API Documentation](./api/) - Mostly worthless if you just want to use
69
74
  __Rack::Cache__ in your application but mildly insightful if you'd like to
70
75
  get a feel for how the system has been put together; I recommend
71
76
  [reading the source](http://github.com/rtomayko/rack-cache/master/lib/rack/cache).
72
77
 
78
+
73
79
  See Also
74
80
  --------
75
81
 
@@ -99,6 +105,7 @@ and is provided under [the MIT license](./license)
99
105
 
100
106
  [config]: ./configuration "Rack::Cache Configuration Language Documentation"
101
107
  [storage]: ./storage "Rack::Cache Storage Documentation"
108
+ [things]: http://tomayko.com/writings/things-caches-do
102
109
 
103
110
  [rfc]: http://tools.ietf.org/html/rfc2616
104
111
  "RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 [ietf.org]"
data/doc/layout.html.erb CHANGED
@@ -14,6 +14,7 @@
14
14
  <p>
15
15
  <a href="./configuration" title='Configuration Language Documentation'>Config</a> |
16
16
  <a href="./storage" title='Cache Storage Documentation'>Storage</a> |
17
+ <a href="http://tomayko.com/writings/things-caches-do" title="Things Caches Do">Things</a> |
17
18
  <a href="./faq" title='Frequently Asked Questions'>FAQ</a> |
18
19
  <a href="./api/" title='Fucking Sucks.'>RDoc</a>
19
20
  </p>
data/doc/server.ru ADDED
@@ -0,0 +1,34 @@
1
+ # Rackup config that serves the contents of Rack::Cache's
2
+ # doc directory. The documentation is rebuilt on each request.
3
+
4
+ # Rewrites URLs like conventional web server configs.
5
+ class Rewriter < Struct.new(:app)
6
+ def call(env)
7
+ if env['PATH_INFO'] =~ /\/$/
8
+ env['PATH_INFO'] += 'index.html'
9
+ elsif env['PATH_INFO'] !~ /\.\w+$/
10
+ env['PATH_INFO'] += '.html'
11
+ end
12
+ app.call(env)
13
+ end
14
+ end
15
+
16
+ # Rebuilds documentation on each request.
17
+ class DocBuilder < Struct.new(:app)
18
+ def call(env)
19
+ if env['PATH_INFO'] !~ /\.(css|js|gif|jpg|png|ico)$/
20
+ env['rack.errors'] << "*** rebuilding documentation (rake -s doc)\n"
21
+ system "rake -s doc"
22
+ end
23
+ app.call(env)
24
+ end
25
+ end
26
+
27
+ use Rack::CommonLogger
28
+ use DocBuilder
29
+ use Rewriter
30
+ use Rack::Static, :root => File.dirname(__FILE__), :urls => ["/"]
31
+
32
+ run(lambda{|env| [404,{},'<h1>Not Found</h1>']})
33
+
34
+ # vim: ft=ruby
@@ -17,7 +17,7 @@
17
17
  #
18
18
  on :receive do
19
19
  pass! unless request.method? 'GET', 'HEAD'
20
- pass! if request.header? 'Cookie', 'Authorization', 'Expect'
20
+ pass! if request.header? 'Expect'
21
21
  lookup!
22
22
  end
23
23
 
@@ -109,7 +109,6 @@ end
109
109
  # * error! - return the error code specified and abandon request.
110
110
  #
111
111
  on :store do
112
- entry.ttl = default_ttl if entry.ttl.nil?
113
112
  trace 'store backend response in cache (ttl: %ds)', entry.ttl
114
113
  persist!
115
114
  end
@@ -74,9 +74,9 @@ module Rack::Cache
74
74
  @triggered.include?(event)
75
75
  end
76
76
 
77
- private
78
77
  # Event handlers.
79
78
  attr_reader :events
79
+ private :events
80
80
 
81
81
  public
82
82
  # Attach custom logic to one or more events.
@@ -101,6 +101,13 @@ module Rack::Cache
101
101
  end
102
102
 
103
103
  private
104
+ # Does the request include authorization or other sensitive information
105
+ # that should cause the response to be considered private by default?
106
+ # Private responses are not stored in the cache.
107
+ def private_request?
108
+ request.header?(*private_headers)
109
+ end
110
+
104
111
  # Determine if the #response validators (ETag, Last-Modified) matches
105
112
  # a conditional value specified in #original_request.
106
113
  def not_modified?
@@ -119,6 +126,7 @@ module Rack::Cache
119
126
  private
120
127
  def perform_receive
121
128
  @original_request = Request.new(@env.dup.freeze)
129
+ @env['REQUEST_METHOD'] = 'GET' if @original_request.head?
122
130
  @request = Request.new(@env)
123
131
  info "%s %s", @original_request.request_method, @original_request.fullpath
124
132
  transition(from=:receive, to=[:pass, :lookup, :error])
@@ -126,6 +134,7 @@ module Rack::Cache
126
134
 
127
135
  def perform_pass
128
136
  trace 'passing'
137
+ request.env['REQUEST_METHOD'] = @original_request.request_method
129
138
  fetch_from_backend
130
139
  transition(from=:pass, to=[:pass, :finish, :error]) do |event|
131
140
  if event == :pass
@@ -191,6 +200,20 @@ module Rack::Cache
191
200
  request.env.delete('HTTP_IF_MODIFIED_SINCE')
192
201
  request.env.delete('HTTP_IF_NONE_MATCH')
193
202
  fetch_from_backend
203
+
204
+ # mark the response as explicitly private if any of the private
205
+ # request headers are present and the response was not explicitly
206
+ # declared public.
207
+ if private_request? && !@response.public?
208
+ @response.private = true
209
+ else
210
+ # assign a default TTL for the cache entry if none was specified in
211
+ # the response; the must-revalidate cache control directive disables
212
+ # default ttl assigment.
213
+ if default_ttl > 0 && @response.ttl.nil? && !@response.must_revalidate?
214
+ @response.ttl = default_ttl
215
+ end
216
+ end
194
217
  transition(from=:fetch, to=[:store, :deliver, :error])
195
218
  end
196
219
 
@@ -198,7 +221,11 @@ module Rack::Cache
198
221
  @entry = @response
199
222
  transition(from=:store, to=[:persist, :deliver, :error]) do |event|
200
223
  if event == :persist
201
- trace "writing response to cache"
224
+ if @response.private?
225
+ warn 'forced to store response marked as private.'
226
+ else
227
+ trace "storing response in cache"
228
+ end
202
229
  metastore.store(original_request, @entry, entitystore)
203
230
  @response = @entry
204
231
  :deliver
@@ -211,6 +238,7 @@ module Rack::Cache
211
238
  def perform_deliver
212
239
  trace "delivering response ..."
213
240
  response.not_modified! if not_modified?
241
+ response.body = [] if @original_request.head?
214
242
  transition(from=:deliver, to=[:finish, :error])
215
243
  end
216
244
 
@@ -58,6 +58,12 @@ module Rack::Cache
58
58
  [key, size]
59
59
  end
60
60
 
61
+ # Remove the body corresponding to key; return nil.
62
+ def purge(key)
63
+ @hash.delete(key)
64
+ nil
65
+ end
66
+
61
67
  def self.resolve(uri)
62
68
  new
63
69
  end
@@ -89,16 +95,19 @@ module Rack::Cache
89
95
  nil
90
96
  end
91
97
 
92
- # Open the entity body and return an IO object. The IO object's
93
- # each method is overridden to read 8K chunks instead of lines.
94
- def open(key)
95
- io = File.open(body_path(key), 'rb')
96
- def io.each
98
+ class Body < ::File #:nodoc:
99
+ def each
97
100
  while part = read(8192)
98
101
  yield part
99
102
  end
100
103
  end
101
- io
104
+ alias_method :to_path, :path
105
+ end
106
+
107
+ # Open the entity body and return an IO object. The IO object's
108
+ # each method is overridden to read 8K chunks instead of lines.
109
+ def open(key)
110
+ Body.open(body_path(key), 'rb')
102
111
  rescue Errno::ENOENT
103
112
  nil
104
113
  end
@@ -121,6 +130,13 @@ module Rack::Cache
121
130
  [key, size]
122
131
  end
123
132
 
133
+ def purge(key)
134
+ File.unlink body_path(key)
135
+ nil
136
+ rescue Errno::ENOENT
137
+ nil
138
+ end
139
+
124
140
  protected
125
141
  def storage_path(stem)
126
142
  File.join root, stem
@@ -190,6 +206,13 @@ module Rack::Cache
190
206
  [key, size]
191
207
  end
192
208
 
209
+ def purge(key)
210
+ cache.delete(key)
211
+ nil
212
+ rescue Memcached::NotFound
213
+ nil
214
+ end
215
+
193
216
  extend Rack::Utils
194
217
 
195
218
  # Create MemCache store for the given URI. The URI must specify
@@ -21,7 +21,7 @@ module Rack::Cache
21
21
  # header is present.
22
22
  def cache_control
23
23
  @cache_control ||=
24
- (headers['Cache-Control'] || '').split(/\s*,\s*/).inject({}) {|hash,token|
24
+ headers['Cache-Control'].to_s.split(/\s*[,;]\s*/).inject({}) {|hash,token|
25
25
  name, value = token.split(/\s*=\s*/, 2)
26
26
  hash[name.downcase] = (value || true) unless name.empty?
27
27
  hash
@@ -107,16 +107,15 @@ module Rack::Cache
107
107
  !fresh?
108
108
  end
109
109
 
110
- # Determine if the response is worth caching under any circumstance. An
111
- # object that is cacheable may not necessary be served from cache without
112
- # first validating the response with the origin.
110
+ # Determine if the response is worth caching under any circumstance. Responses
111
+ # marked "private" with an explicit Cache-Control directive are considered
112
+ # uncacheable
113
113
  #
114
- # An object that includes no freshness lifetime (Expires, max-age) and that
115
- # does not include a validator (Last-Modified, Etag) serves no purpose in a
116
- # cache that only serves fresh or valid objects.
114
+ # Responses with neither a freshness lifetime (Expires, max-age) nor cache
115
+ # validator (Last-Modified, Etag) are considered uncacheable.
117
116
  def cacheable?
118
117
  return false unless CACHEABLE_RESPONSE_CODES.include?(status)
119
- return false if no_store?
118
+ return false if no_store? || private?
120
119
  validateable? || fresh?
121
120
  end
122
121
 
@@ -124,7 +123,8 @@ module Rack::Cache
124
123
  # a +Cache-Control+ header with +max-age+ value is present or when the
125
124
  # +Expires+ header is set.
126
125
  def freshness_information?
127
- header?('Expires') || !cache_control['max-age'].nil?
126
+ header?('Expires') ||
127
+ !!(cache_control['s-maxage'] || cache_control['max-age'])
128
128
  end
129
129
 
130
130
  # Determine if the response includes headers that can be used to validate
@@ -137,7 +137,7 @@ module Rack::Cache
137
137
  # revalidating with the origin. Note that this does not necessary imply that
138
138
  # a caching agent ought not store the response in its cache.
139
139
  def no_cache?
140
- !cache_control['no-cache'].nil?
140
+ cache_control['no-cache']
141
141
  end
142
142
 
143
143
  # Indicates that the response should not be stored under any circumstances.
@@ -145,6 +145,42 @@ module Rack::Cache
145
145
  cache_control['no-store']
146
146
  end
147
147
 
148
+ # True when the response has been explicitly marked "public".
149
+ def public?
150
+ cache_control['public']
151
+ end
152
+
153
+ # Mark the response "public", making it eligible for other clients. Note
154
+ # that responses are considered "public" by default unless the request
155
+ # includes private headers (Authorization, Cookie).
156
+ def public=(value)
157
+ value = value ? true : nil
158
+ self.cache_control = cache_control.
159
+ merge('public' => value, 'private' => !value)
160
+ end
161
+
162
+ # True when the response has been marked "private" explicitly.
163
+ def private?
164
+ cache_control['private']
165
+ end
166
+
167
+ # Mark the response "private", making it ineligible for serving other
168
+ # clients.
169
+ def private=(value)
170
+ value = value ? true : nil
171
+ self.cache_control = cache_control.
172
+ merge('public' => !value, 'private' => value)
173
+ end
174
+
175
+ # Indicates that the cache must not serve a stale response in any
176
+ # circumstance without first revalidating with the origin. When present,
177
+ # the TTL of the response should not be overriden to be greater than the
178
+ # value provided by the origin.
179
+ def must_revalidate?
180
+ cache_control['must-revalidate'] ||
181
+ cache_control['proxy-revalidate']
182
+ end
183
+
148
184
  # The date, as specified by the Date header. When no Date header is present,
149
185
  # set the Date header to Time.now and return.
150
186
  def date
@@ -163,29 +199,36 @@ module Rack::Cache
163
199
 
164
200
  # The number of seconds after the time specified in the response's Date
165
201
  # header when the the response should no longer be considered fresh. First
166
- # check for a Cache-Control max-age value, and fall back on an expires
167
- # header; return nil when no maximum age can be established.
202
+ # check for a s-maxage directive, then a max-age directive, and then fall
203
+ # back on an expires header; return nil when no maximum age can be
204
+ # established.
168
205
  def max_age
169
- if age = cache_control['max-age']
206
+ if age = (cache_control['s-maxage'] || cache_control['max-age'])
170
207
  age.to_i
171
208
  elsif headers['Expires']
172
209
  Time.httpdate(headers['Expires']) - date
173
210
  end
174
211
  end
175
212
 
176
- # Sets the number of seconds after which the response should no longer
177
- # be considered fresh. This sets the Cache-Control max-age value.
213
+ # The number of seconds after which the response should no longer
214
+ # be considered fresh. Sets the Cache-Control max-age directive.
178
215
  def max_age=(value)
179
216
  self.cache_control = cache_control.merge('max-age' => value.to_s)
180
217
  end
181
218
 
219
+ # Like #max_age= but sets the s-maxage directive, which applies only
220
+ # to shared caches.
221
+ def shared_max_age=(value)
222
+ self.cache_control = cache_control.merge('s-maxage' => value.to_s)
223
+ end
224
+
182
225
  # The Time when the response should be considered stale. With a
183
226
  # Cache-Control/max-age value is present, this is calculated by adding the
184
227
  # number of seconds specified to the responses #date value. Falls back to
185
228
  # the time specified in the Expires header or returns nil if neither is
186
229
  # present.
187
230
  def expires_at
188
- if max_age = cache_control['max-age']
231
+ if max_age = (cache_control['s-maxage'] || cache_control['max-age'])
189
232
  date + max_age.to_i
190
233
  elsif time = headers['Expires']
191
234
  Time.httpdate(time)
@@ -200,9 +243,15 @@ module Rack::Cache
200
243
  max_age - age if max_age
201
244
  end
202
245
 
203
- # Set the response's time-to-live to the specified number of seconds. This
204
- # adjusts the Cache-Control/max-age value.
246
+ # Set the response's time-to-live for shared caches to the specified number
247
+ # of seconds. This adjusts the Cache-Control/s-maxage directive.
205
248
  def ttl=(seconds)
249
+ self.shared_max_age = age + seconds
250
+ end
251
+
252
+ # Set the response's time-to-live for private/client caches. This adjusts
253
+ # the Cache-Control/max-age directive.
254
+ def client_ttl=(seconds)
206
255
  self.max_age = age + seconds
207
256
  end
208
257
 
@@ -250,8 +299,7 @@ module Rack::Cache
250
299
  nil
251
300
  end
252
301
 
253
- # The literal value of the Vary header, or nil when no Vary header is
254
- # present.
302
+ # The literal value of the Vary header, or nil when no header is present.
255
303
  def vary
256
304
  headers['Vary']
257
305
  end