class Rack::Cache::Context

Implements Rack's middleware interface and provides the context for all cache logic, including the core logic engine.

Attributes

backend[R]

The Rack application object immediately downstream.

trace[R]

Array of trace Symbols

Public Class Methods

new(backend, options={}) { |self| ... } click to toggle source
   # File lib/rack/cache/context.rb
18 def initialize(backend, options={})
19   @backend = backend
20   @trace = []
21   @env = nil
22 
23   initialize_options options
24   yield self if block_given?
25 
26   @private_header_keys =
27     private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
28 end

Public Instance Methods

call(env) click to toggle source

The Rack call interface. The receiver acts as a prototype and runs each request in a dup object unless the rack.run_once variable is set in the environment.

   # File lib/rack/cache/context.rb
47 def call(env)
48   if env['rack.run_once'] && !env['rack.multithread']
49     call! env
50   else
51     clone.call! env
52   end
53 end
call!(env) click to toggle source

The real Rack call interface. The caching logic is performed within the context of the receiver.

   # File lib/rack/cache/context.rb
57 def call!(env)
58   @trace = []
59   @default_options.each { |k,v| env[k] ||= v }
60   @env = env
61   @request = Request.new(@env.dup.freeze)
62 
63   response =
64     if @request.get? || @request.head?
65       if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
66         lookup
67       else
68         pass
69       end
70     else
71       if @request.options?
72         pass
73       else
74         invalidate
75       end
76     end
77 
78   # log trace and set X-Rack-Cache tracing header
79   trace = @trace.join(', ')
80   response.headers['X-Rack-Cache'] = trace
81 
82   # write log message to rack.errors
83   if verbose?
84     message = "cache: [%s %s] %s\n" %
85       [@request.request_method, @request.fullpath, trace]
86     log_info(message)
87   end
88 
89   # tidy up response a bit
90   if (@request.get? || @request.head?) && not_modified?(response)
91     response.not_modified!
92   end
93 
94   if @request.head?
95     response.body.close if response.body.respond_to?(:close)
96     response.body = []
97   end
98   response.to_a
99 end
entitystore() click to toggle source

The configured EntityStore instance. Changing the rack-cache.entitystore value effects the result of this method immediately.

   # File lib/rack/cache/context.rb
39 def entitystore
40   uri = options['rack-cache.entitystore']
41   storage.resolve_entitystore_uri(uri)
42 end
metastore() click to toggle source

The configured MetaStore instance. Changing the rack-cache.metastore value effects the result of this method immediately.

   # File lib/rack/cache/context.rb
32 def metastore
33   uri = options['rack-cache.metastore']
34   storage.resolve_metastore_uri(uri)
35 end

Private Instance Methods

convert_head_to_get!() click to toggle source

send no head requests because we want content

    # File lib/rack/cache/context.rb
304 def convert_head_to_get!
305   if @env['REQUEST_METHOD'] == 'HEAD'
306     @env['REQUEST_METHOD'] = 'GET'
307     @env['rack.methodoverride.original_method'] = 'HEAD'
308   end
309 end
fetch() click to toggle source

The cache missed or a reload is required. Forward the request to the backend and determine whether the response should be stored. This allows conditional / validation requests through to the backend but performs no caching of the response when the backend returns a 304.

    # File lib/rack/cache/context.rb
245 def fetch
246   # send no head requests because we want content
247   convert_head_to_get!
248 
249   response = forward
250 
251   # Mark the response as explicitly private if any of the private
252   # request headers are present and the response was not explicitly
253   # declared public.
254   if private_request? && !response.cache_control.public?
255     response.private = true
256   elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
257     # assign a default TTL for the cache entry if none was specified in
258     # the response; the must-revalidate cache control directive disables
259     # default ttl assigment.
260     response.ttl = default_ttl
261   end
262 
263   store(response) if response.cacheable?
264 
265   response
266 end
forward() click to toggle source

Delegate the request to the backend and create the response.

    # File lib/rack/cache/context.rb
139 def forward
140   Response.new(*backend.call(@env))
141 end
fresh_enough?(entry) click to toggle source

Whether the cache entry is “fresh enough” to satisfy the request.

    # File lib/rack/cache/context.rb
128 def fresh_enough?(entry)
129   if entry.fresh?
130     if allow_revalidate? && max_age = @request.cache_control.max_age
131       max_age > 0 && max_age >= entry.age
132     else
133       true
134     end
135   end
136 end
invalidate() click to toggle source

Invalidate POST, PUT, DELETE and all methods not understood by this cache See RFC2616 13.10

    # File lib/rack/cache/context.rb
152 def invalidate
153   metastore.invalidate(@request, entitystore)
154 rescue => e
155   log_error(e)
156   pass
157 else
158   record :invalidate
159   pass
160 end
log(level, message) click to toggle source
    # File lib/rack/cache/context.rb
295 def log(level, message)
296   if @env['rack.logger']
297     @env['rack.logger'].send(level, message)
298   else
299     @env['rack.errors'].write(message)
300   end
301 end
log_error(exception) click to toggle source
    # File lib/rack/cache/context.rb
286 def log_error(exception)
287   message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n"
288   log(:error, message)
289 end
log_info(message) click to toggle source
    # File lib/rack/cache/context.rb
291 def log_info(message)
292   log(:info, message)
293 end
lookup() click to toggle source

Try to serve the response from cache. When a matching cache entry is found and is fresh, use it as the response without forwarding any request to the backend. When a matching cache entry is found but is stale, attempt to validate the entry with the backend using conditional GET. When no matching cache entry is found, trigger miss processing.

    # File lib/rack/cache/context.rb
167 def lookup
168   if @request.no_cache? && allow_reload?
169     record :reload
170     fetch
171   else
172     begin
173       entry = metastore.lookup(@request, entitystore)
174     rescue => e
175       log_error(e)
176       return pass
177     end
178     if entry
179       if fresh_enough?(entry)
180         record :fresh
181         entry.headers['Age'] = entry.age.to_s
182         entry
183       else
184         record :stale
185         validate(entry)
186       end
187     else
188       record :miss
189       fetch
190     end
191   end
192 end
not_modified?(response) click to toggle source

Determine if the response validators (ETag, Last-Modified) matches a conditional value specified in request.

    # File lib/rack/cache/context.rb
117 def not_modified?(response)
118   last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
119   if etags = @request.env['HTTP_IF_NONE_MATCH']
120     etags = etags.split(/\s*,\s*/)
121     (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
122   elsif last_modified
123     response.last_modified == last_modified
124   end
125 end
pass() click to toggle source

The request is sent to the backend, and the backend's response is sent to the client, but is not entered into the cache.

    # File lib/rack/cache/context.rb
145 def pass
146   record :pass
147   forward
148 end
private_request?() click to toggle source

Does the request include authorization or other sensitive information that should cause the response to be considered private by default? Private responses are not stored in the cache.

    # File lib/rack/cache/context.rb
111 def private_request?
112   @private_header_keys.any? { |key| @env.key?(key) }
113 end
record(event) click to toggle source

Record that an event took place.

    # File lib/rack/cache/context.rb
104 def record(event)
105   @trace << event
106 end
store(response) click to toggle source

Write the response to the cache.

    # File lib/rack/cache/context.rb
269 def store(response)
270   strip_ignore_headers(response)
271   metastore.store(@request, response, entitystore)
272   response.headers['Age'] = response.age.to_s
273 rescue => e
274   log_error(e)
275   nil
276 else
277   record :store
278 end
strip_ignore_headers(response) click to toggle source

Remove all ignored response headers before writing to the cache.

    # File lib/rack/cache/context.rb
281 def strip_ignore_headers(response)
282   stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
283   record :ignore if stripped_values.any?
284 end
validate(entry) click to toggle source

Validate that the cache entry is fresh. The original request is used as a template for a conditional GET request with the backend.

    # File lib/rack/cache/context.rb
196 def validate(entry)
197   # send no head requests because we want content
198   convert_head_to_get!
199 
200   # add our cached last-modified validator to the environment
201   @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
202 
203   # Add our cached etag validator to the environment.
204   # We keep the etags from the client to handle the case when the client
205   # has a different private valid entry which is not cached here.
206   cached_etags = entry.etag.to_s.split(/\s*,\s*/)
207   request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
208   etags = (cached_etags + request_etags).uniq
209   @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
210 
211   response = forward
212 
213   if response.status == 304
214     record :valid
215 
216     # Check if the response validated which is not cached here
217     etag = response.headers['ETag']
218     return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)
219 
220     entry = entry.dup
221     entry.headers.delete('Date')
222     %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
223       next unless value = response.headers[name]
224       entry.headers[name] = value
225     end
226 
227     # even though it's empty, be sure to close the response body from upstream
228     # because middleware use close to signal end of response
229     response.body.close if response.body.respond_to?(:close)
230 
231     response = entry
232   else
233     record :invalid
234   end
235 
236   store(response) if response.cacheable?
237 
238   response
239 end