class Rack::Cache::Context
Implements Rack’s middleware interface and provides the context for all cache logic, including the core logic engine.
Attributes
The Rack
application object immediately downstream.
Array of trace Symbols
Public Class Methods
# File lib/rack/cache/context.rb 18 def initialize(backend, options={}) 19 @backend = backend 20 @trace = [] 21 @env = nil 22 @options = options 23 24 initialize_options options 25 yield self if block_given? 26 27 @private_header_keys = 28 private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" } 29 end
Public Instance Methods
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 48 def call(env) 49 if env['rack.run_once'] && !env['rack.multithread'] 50 call! env 51 else 52 clone.call! env 53 end 54 end
The real Rack
call interface. The caching logic is performed within the context of the receiver.
# File lib/rack/cache/context.rb 58 def call!(env) 59 @trace = [] 60 @default_options.each { |k,v| env[k] ||= v } 61 @env = env 62 @request = Request.new(@env.dup.freeze) 63 64 response = 65 if @request.get? || @request.head? 66 if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass'] 67 lookup 68 else 69 pass 70 end 71 else 72 if @request.options? 73 pass 74 else 75 invalidate 76 end 77 end 78 79 # log trace and set x-rack-cache tracing header 80 trace = @trace.join(', ') 81 response.headers['x-rack-cache'] = trace 82 83 # write log message to rack.errors 84 if verbose? 85 message = "cache: [%s %s] %s\n" % 86 [@request.request_method, @request.fullpath, trace] 87 log_info(message) 88 end 89 90 # tidy up response a bit 91 if (@request.get? || @request.head?) && not_modified?(response) 92 response.not_modified! 93 end 94 95 if @request.head? 96 response.body.close if response.body.respond_to?(:close) 97 response.body = [] 98 end 99 response.to_a 100 end
The configured EntityStore
instance. Changing the rack-cache.entitystore value effects the result of this method immediately.
# File lib/rack/cache/context.rb 40 def entitystore 41 uri = options['rack-cache.entitystore'] 42 storage.resolve_entitystore_uri(uri, @options) 43 end
The configured MetaStore
instance. Changing the rack-cache.metastore value effects the result of this method immediately.
# File lib/rack/cache/context.rb 33 def metastore 34 uri = options['rack-cache.metastore'] 35 storage.resolve_metastore_uri(uri, @options) 36 end
Private Instance Methods
send no head requests because we want content
# File lib/rack/cache/context.rb 322 def convert_head_to_get! 323 if @env['REQUEST_METHOD'] == 'HEAD' 324 @env['REQUEST_METHOD'] = 'GET' 325 @env['rack.methodoverride.original_method'] = 'HEAD' 326 end 327 end
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 263 def fetch 264 # send no head requests because we want content 265 convert_head_to_get! 266 267 response = forward 268 269 # Mark the response as explicitly private if any of the private 270 # request headers are present and the response was not explicitly 271 # declared public. 272 if private_request? && !response.cache_control.public? 273 response.private = true 274 elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate? 275 # assign a default TTL for the cache entry if none was specified in 276 # the response; the must-revalidate cache control directive disables 277 # default ttl assigment. 278 response.ttl = default_ttl 279 end 280 281 store(response) if response.cacheable? 282 283 response 284 end
Delegate the request to the backend and create the response.
# File lib/rack/cache/context.rb 140 def forward 141 Response.new(*backend.call(@env)) 142 end
Whether the cache entry is “fresh enough” to satisfy the request.
# File lib/rack/cache/context.rb 129 def fresh_enough?(entry) 130 if entry.fresh? 131 if allow_revalidate? && max_age = @request.cache_control.max_age 132 max_age > 0 && max_age >= entry.age 133 else 134 true 135 end 136 end 137 end
Invalidate POST, PUT, DELETE and all methods not understood by this cache See RFC2616 13.10
# File lib/rack/cache/context.rb 153 def invalidate 154 metastore.invalidate(@request, entitystore) 155 rescue => e 156 log_error(e) 157 pass 158 else 159 record :invalidate 160 pass 161 end
# File lib/rack/cache/context.rb 313 def log(level, message) 314 if @env['rack.logger'] 315 @env['rack.logger'].send(level, message) 316 else 317 @env['rack.errors'].write(message) 318 end 319 end
# File lib/rack/cache/context.rb 304 def log_error(exception) 305 message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n" 306 log(:error, message) 307 end
# File lib/rack/cache/context.rb 309 def log_info(message) 310 log(:info, message) 311 end
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. If validation raises an exception and fault tolerant caching is enabled, serve the stale cache entry. When no matching cache entry is found, trigger miss processing.
# File lib/rack/cache/context.rb 170 def lookup 171 if @request.no_cache? && allow_reload? 172 record :reload 173 fetch 174 else 175 begin 176 entry = metastore.lookup(@request, entitystore) 177 rescue => e 178 log_error(e) 179 return pass 180 end 181 if entry 182 if fresh_enough?(entry) 183 record :fresh 184 entry.headers['age'] = entry.age.to_s 185 entry 186 else 187 record :stale 188 if fault_tolerant? 189 validate_with_stale_cache_failover(entry) 190 else 191 validate(entry) 192 end 193 end 194 else 195 record :miss 196 fetch 197 end 198 end 199 end
Determine if the response validators (etag, last-modified) matches a conditional value specified in request.
# File lib/rack/cache/context.rb 118 def not_modified?(response) 119 last_modified = @request.env['HTTP_IF_MODIFIED_SINCE'] 120 if etags = @request.env['HTTP_IF_NONE_MATCH'] 121 etags = etags.split(/\s*,\s*/) 122 (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified) 123 elsif last_modified 124 response.last_modified == last_modified 125 end 126 end
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 146 def pass 147 record :pass 148 forward 149 end
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 112 def private_request? 113 @private_header_keys.any? { |key| @env.key?(key) } 114 end
Record that an event took place.
# File lib/rack/cache/context.rb 105 def record(event) 106 @trace << event 107 end
Write the response to the cache.
# File lib/rack/cache/context.rb 287 def store(response) 288 strip_ignore_headers(response) 289 metastore.store(@request, response, entitystore) 290 response.headers['age'] = response.age.to_s 291 rescue => e 292 log_error(e) 293 nil 294 else 295 record :store 296 end
Remove all ignored response headers before writing to the cache.
# File lib/rack/cache/context.rb 299 def strip_ignore_headers(response) 300 stripped_values = ignore_headers.map { |name| response.headers.delete(name) } 301 record :ignore if stripped_values.any? 302 end
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 214 def validate(entry) 215 # send no head requests because we want content 216 convert_head_to_get! 217 218 # add our cached last-modified validator to the environment 219 @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified 220 221 # Add our cached etag validator to the environment. 222 # We keep the etags from the client to handle the case when the client 223 # has a different private valid entry which is not cached here. 224 cached_etags = entry.etag.to_s.split(/\s*,\s*/) 225 request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/) 226 etags = (cached_etags + request_etags).uniq 227 @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ') 228 229 response = forward 230 231 if response.status == 304 232 record :valid 233 234 # Check if the response validated which is not cached here 235 etag = response.headers['etag'] 236 return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag) 237 238 entry = entry.dup 239 entry.headers.delete('date') 240 %w[Date expires cache-control etag last-modified].each do |name| 241 next unless value = response.headers[name] 242 entry.headers[name] = value 243 end 244 245 # even though it's empty, be sure to close the response body from upstream 246 # because middleware use close to signal end of response 247 response.body.close if response.body.respond_to?(:close) 248 249 response = entry 250 else 251 record :invalid 252 end 253 254 store(response) if response.cacheable? 255 256 response 257 end
Returns stale cache on exception.
# File lib/rack/cache/context.rb 202 def validate_with_stale_cache_failover(entry) 203 validate(entry) 204 rescue => e 205 record :connnection_failed 206 age = entry.age.to_s 207 entry.headers['age'] = age 208 record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e}" 209 entry 210 end