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 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
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
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
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
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
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
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
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
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 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
# 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
# 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
# File lib/rack/cache/context.rb 291 def log_info(message) 292 log(:info, message) 293 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. 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
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
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
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 that an event took place.
# File lib/rack/cache/context.rb 104 def record(event) 105 @trace << event 106 end
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
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 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