lib/rack/cache/context.rb in rack-cache-0.3.0 vs lib/rack/cache/context.rb in rack-cache-0.4

- old
+ new

@@ -1,95 +1,233 @@ -require 'rack/cache/config' require 'rack/cache/options' -require 'rack/cache/core' require 'rack/cache/request' require 'rack/cache/response' require 'rack/cache/storage' module Rack::Cache # Implements Rack's middleware interface and provides the context for all - # cache logic. This class includes the Options, Config, and Core modules - # to provide much of its core functionality. - + # cache logic, including the core logic engine. class Context include Rack::Cache::Options - include Rack::Cache::Config - include Rack::Cache::Core + # Array of trace Symbols + attr_reader :trace + # The Rack application object immediately downstream. attr_reader :backend - def initialize(backend, options={}, &block) - @errors = nil - @env = nil + def initialize(backend, options={}) @backend = backend + @trace = [] + initialize_options options - initialize_core - initialize_config(&block) + yield self if block_given? + + @private_header_keys = + private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" } end - # The call! method is invoked on the duplicate context instance. - # process_request is defined in Core. - alias_method :call!, :process_request - protected :call! + # The configured MetaStore instance. Changing the rack-cache.metastore + # value effects the result of this method immediately. + def metastore + uri = options['rack-cache.metastore'] + storage.resolve_metastore_uri(uri) + end - # The Rack call interface. The receiver acts as a prototype and runs each - # request in a duplicate object, unless the +rack.run_once+ variable is set - # in the environment. + # The configured EntityStore instance. Changing the rack-cache.entitystore + # value effects the result of this method immediately. + def entitystore + uri = options['rack-cache.entitystore'] + storage.resolve_entitystore_uri(uri) + end + + # 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. def call(env) if env['rack.run_once'] call! env else clone.call! env end end - public - # IO-like object that receives log, warning, and error messages; - # defaults to the rack.errors environment variable. - def errors - @errors || (@env && (@errors = @env['rack.errors'])) || STDERR + # The real Rack call interface. The caching logic is performed within + # the context of the receiver. + def call!(env) + @trace = [] + @env = @default_options.merge(env) + @request = Request.new(@env.dup.freeze) + + response = + if @request.get? || @request.head? + if !@env['HTTP_EXPECT'] + lookup + else + pass + end + else + invalidate + end + + # log trace and set X-Rack-Cache tracing header + trace = @trace.join(', ') + response.headers['X-Rack-Cache'] = trace + + # write log message to rack.errors + if verbose? + message = "cache: [%s %s] %s\n" % + [@request.request_method, @request.fullpath, trace] + @env['rack.errors'].write(message) + end + + # tidy up response a bit + response.not_modified! if not_modified?(response) + response.body = [] if @request.head? + response.to_a end - # Set the output stream for log messages, warnings, and errors. - def errors=(ioish) - fail "stream must respond to :write" if ! ioish.respond_to?(:write) - @errors = ioish + private + + # Record that an event took place. + def record(event) + @trace << event end - # The configured MetaStore instance. Changing the rack-cache.metastore - # environment variable effects the result of this method immediately. - def metastore - uri = options['rack-cache.metastore'] - storage.resolve_metastore_uri(uri) + # 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. + def private_request? + @private_header_keys.any? { |key| @env.key?(key) } end - # The configured EntityStore instance. Changing the rack-cache.entitystore - # environment variable effects the result of this method immediately. - def entitystore - uri = options['rack-cache.entitystore'] - storage.resolve_entitystore_uri(uri) + # Determine if the #response validators (ETag, Last-Modified) matches + # a conditional value specified in #request. + def not_modified?(response) + response.etag_matches?(@request.env['HTTP_IF_NONE_MATCH']) || + response.last_modified_at?(@request.env['HTTP_IF_MODIFIED_SINCE']) end - protected - # Write a log message to the errors stream. +level+ is a symbol - # such as :error, :warn, :info, or :trace. - def log(level, message=nil, *params) - errors.write("[cache] #{level}: #{message}\n" % params) - errors.flush + # Whether the cache entry is "fresh enough" to satisfy the request. + def fresh_enough?(entry) + if entry.fresh? + if allow_revalidate? && max_age = @request.cache_control.max_age + max_age > 0 && max_age >= entry.age + else + true + end + end end - def info(*message, &bk) - log :info, *message, &bk + # Delegate the request to the backend and create the response. + def forward + Response.new(*backend.call(@env)) end - def warn(*message, &bk) - log :warn, *message, &bk + # The request is sent to the backend, and the backend's response is sent + # to the client, but is not entered into the cache. + def pass + record :pass + forward end - def trace(*message, &bk) - return unless verbose? - log :trace, *message, &bk + # Invalidate POST, PUT, DELETE and all methods not understood by this cache + # See RFC2616 13.10 + def invalidate + record :invalidate + metastore.invalidate(@request, entitystore) + pass end - 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. + def lookup + if @request.no_cache? && allow_reload? + record :reload + fetch + elsif entry = metastore.lookup(@request, entitystore) + if fresh_enough?(entry) + record :fresh + entry.headers['Age'] = entry.age.to_s + entry + else + record :stale + validate(entry) + end + else + record :miss + fetch + end + end + + # Validate that the cache entry is fresh. The original request is used + # as a template for a conditional GET request with the backend. + def validate(entry) + # send no head requests because we want content + @env['REQUEST_METHOD'] = 'GET' + + # add our cached validators to the environment + @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified + @env['HTTP_IF_NONE_MATCH'] = entry.etag + + backend_response = forward + + response = + if backend_response.status == 304 + record :valid + entry = entry.dup + entry.headers.delete('Date') + %w[Date Expires Cache-Control ETag Last-Modified].each do |name| + next unless value = backend_response.headers[name] + entry.headers[name] = value + end + entry + else + record :invalid + backend_response + end + + store(response) if response.cacheable? + + response + end + + # The cache missed or a reload is required. Forward the request to the + # backend and determine whether the response should be stored. + def fetch + # send no head requests because we want content + @env['REQUEST_METHOD'] = 'GET' + + # avoid that the backend sends no content + @env.delete('HTTP_IF_MODIFIED_SINCE') + @env.delete('HTTP_IF_NONE_MATCH') + + response = forward + + # Mark the response as explicitly private if any of the private + # request headers are present and the response was not explicitly + # declared public. + if private_request? && !response.cache_control.public? + response.private = true + elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate? + # assign a default TTL for the cache entry if none was specified in + # the response; the must-revalidate cache control directive disables + # default ttl assigment. + response.ttl = default_ttl + end + + store(response) if response.cacheable? + + response + end + + # Write the response to the cache. + def store(response) + record :store + metastore.store(@request, response, entitystore) + response.headers['Age'] = response.age.to_s + end + end end