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, line 18
def initialize(backend, options={})
  @backend = backend
  @trace = []
  @env = nil

  initialize_options options
  yield self if block_given?

  @private_header_keys =
    private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
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, line 47
def call(env)
  if env['rack.run_once'] && !env['rack.multithread']
    call! env
  else
    clone.call! env
  end
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, line 57
def call!(env)
  @trace = []
  @default_options.each { |k,v| env[k] ||= v }
  @env = env
  @request = Request.new(@env.dup.freeze)

  response =
    if @request.get? || @request.head?
      if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
        lookup
      else
        pass
      end
    else
      if @request.options?
        pass
      else
        invalidate
      end
    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]
    log_info(message)
  end

  # tidy up response a bit
  if (@request.get? || @request.head?) && not_modified?(response)
    response.not_modified!
  end

  if @request.head?
    response.body.close if response.body.respond_to?(:close)
    response.body = []
  end
  response.to_a
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, line 39
def entitystore
  uri = options['rack-cache.entitystore']
  storage.resolve_entitystore_uri(uri)
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, line 32
def metastore
  uri = options['rack-cache.metastore']
  storage.resolve_metastore_uri(uri)
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, line 304
def convert_head_to_get!
  if @env['REQUEST_METHOD'] == 'HEAD'
    @env['REQUEST_METHOD'] = 'GET'
    @env['rack.methodoverride.original_method'] = 'HEAD'
  end
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, line 245
def fetch
  # send no head requests because we want content
  convert_head_to_get!

  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
forward() click to toggle source

Delegate the request to the backend and create the response.

# File lib/rack/cache/context.rb, line 139
def forward
  Response.new(*backend.call(@env))
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, line 128
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
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, line 152
def invalidate
  metastore.invalidate(@request, entitystore)
rescue => e
  log_error(e)
  pass
else
  record :invalidate
  pass
end
log(level, message) click to toggle source
# File lib/rack/cache/context.rb, line 295
def log(level, message)
  if @env['rack.logger']
    @env['rack.logger'].send(level, message)
  else
    @env['rack.errors'].write(message)
  end
end
log_error(exception) click to toggle source
# File lib/rack/cache/context.rb, line 286
def log_error(exception)
  message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n"
  log(:error, message)
end
log_info(message) click to toggle source
# File lib/rack/cache/context.rb, line 291
def log_info(message)
  log(:info, message)
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, line 167
def lookup
  if @request.no_cache? && allow_reload?
    record :reload
    fetch
  else
    begin
      entry = metastore.lookup(@request, entitystore)
    rescue => e
      log_error(e)
      return pass
    end
    if entry
      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
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, line 117
def not_modified?(response)
  last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
  if etags = @request.env['HTTP_IF_NONE_MATCH']
    etags = etags.split(/\s*,\s*/)
    (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
  elsif last_modified
    response.last_modified == last_modified
  end
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, line 145
def pass
  record :pass
  forward
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, line 111
def private_request?
  @private_header_keys.any? { |key| @env.key?(key) }
end
record(event) click to toggle source

Record that an event took place.

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

Write the response to the cache.

# File lib/rack/cache/context.rb, line 269
def store(response)
  strip_ignore_headers(response)
  metastore.store(@request, response, entitystore)
  response.headers['Age'] = response.age.to_s
rescue => e
  log_error(e)
  nil
else
  record :store
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, line 281
def strip_ignore_headers(response)
  stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
  record :ignore if stripped_values.any?
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, line 196
def validate(entry)
  # send no head requests because we want content
  convert_head_to_get!

  # add our cached last-modified validator to the environment
  @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified

  # Add our cached etag validator to the environment.
  # We keep the etags from the client to handle the case when the client
  # has a different private valid entry which is not cached here.
  cached_etags = entry.etag.to_s.split(/\s*,\s*/)
  request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
  etags = (cached_etags + request_etags).uniq
  @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')

  response = forward

  if response.status == 304
    record :valid

    # Check if the response validated which is not cached here
    etag = response.headers['ETag']
    return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)

    entry = entry.dup
    entry.headers.delete('Date')
    %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
      next unless value = response.headers[name]
      entry.headers[name] = value
    end

    # even though it's empty, be sure to close the response body from upstream
    # because middleware use close to signal end of response
    response.body.close if response.body.respond_to?(:close)

    response = entry
  else
    record :invalid
  end

  store(response) if response.cacheable?

  response
end