rtomayko-rack-cache 0.2.0 → 0.3.0

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.
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