lib/locomotive/steam/middlewares/cache.rb
module Locomotive::Steam
module Middlewares
class Cache < ThreadSafe
include Concerns::Helpers
CACHEABLE_RESPONSE_CODES = [200, 301, 404, 410].freeze
CACHEABLE_REQUEST_METHODS = %w(GET HEAD).freeze
DEFAULT_CACHE_CONTROL = 'max-age=0, s-maxage=3600, public, must-revalidate'.freeze
DEFAULT_CACHE_VARY = 'Accept-Language'.freeze
NO_CACHE_CONTROL = 'max-age=0, private, must-revalidate'.freeze
def _call
if cacheable?
key = cache_key
# TODO: only for debugging
# log("HTTP keys: #{env.select { |key, _| key.starts_with?('HTTP_') }}".light_blue)
# Test if the ETag or Last Modified has been modified. If not, return a 304 response
if stale?(key)
render_response(nil, 304, nil)
return
end
# we have to tell the CDN (or any proxy) what the expiration & validation strategy are
env['steam.cache_control'] = cache_control
env['steam.cache_vary'] = cache_vary
env['steam.cache_etag'] = key
env['steam.cache_last_modified'] = site.last_modified_at.httpdate
# retrieve the response from the cache.
# This is useful if no CDN is being used.
code, headers, _ = response = fetch_cached_response(key)
unless CACHEABLE_RESPONSE_CODES.include?(code.to_i)
env['steam.cache_control'] = headers['cache-control'] = NO_CACHE_CONTROL
env['steam.cache_vary'] = headers['Vary'] = nil
end
# we don't want to render twice the page
@next_response = response
else
env['steam.cache_control'] = NO_CACHE_CONTROL
end
end
private
def fetch_cached_response(key)
log("Cache key = #{key.inspect}")
if marshaled = cache.read(key)
log('Cache HIT')
Marshal.load(marshaled)
else
log('Cache MISS')
self.next.tap do |response|
# cache the HTML for further validations (+ optimization)
cache.write(key, marshal(response))
end
end
end
def cacheable?
CACHEABLE_REQUEST_METHODS.include?(env['REQUEST_METHOD']) &&
!live_editing? &&
site.try(:cache_enabled) &&
page.try(:cache_enabled) &&
is_redirect_url?
end
def cache_key
site, path, query = env['steam.site'], env['PATH_INFO'], env['QUERY_STRING']
key = "#{Locomotive::Steam::VERSION}/site/#{site._id}/#{site.last_modified_at.to_i}/page/#{path}/#{query}"
Digest::MD5.hexdigest(key)
end
def cache_control
page.try(:cache_control).presence || site.try(:cache_control).presence || DEFAULT_CACHE_CONTROL
end
def cache_vary
page.try(:cache_vary).presence || site.try(:cache_vary).presence || DEFAULT_CACHE_VARY
end
def is_redirect_url?
return false if page.nil?
page.try(:redirect_url).blank?
end
def marshal(response)
code, headers, body = response
# only keep string value headers
_headers = headers.reject { |key, val| !val.respond_to?(:to_str) }
Marshal.dump([code, _headers, body])
end
def stale?(key)
env['HTTP_IF_NONE_MATCH'] == key ||
env['HTTP_IF_MODIFIED_SINCE'] == site.last_modified_at.httpdate
end
def cache
services.cache
end
end
end
end