class Async::HTTP::Cache::General
Implements a general shared cache according to www.rfc-editor.org/rfc/rfc9111
def cacheable_request?(request)
def cacheable_request?(request) # We don't support caching requests which have a request body: if request.body return false end # We can't cache upgraded requests: if request.protocol return false end # We only support caching GET and HEAD requests: unless request.method == "GET" || request.method == "HEAD" return false end if request.headers[AUTHORIZATION] return false end if request.headers[COOKIE] return false end # Otherwise, we can cache it: return true end
def cacheable_response?(response)
def cacheable_response?(response) # At this point, we know response.status and response.headers. # But we don't know response.body or response.headers.trailer. unless CACHEABLE_RESPONSE_CODES.include?(response.status) Console.logger.debug(self, status: response.status) {"Cannot cache response with status code!"} return false end unless cacheable_response_headers?(response.headers) Console.logger.debug(self) {"Cannot cache response with uncacheable headers!"} return false end return true end
def cacheable_response_headers?(headers)
def cacheable_response_headers?(headers) if cache_control = headers[CACHE_CONTROL] if cache_control.no_store? || cache_control.private? Console.logger.debug(self, cache_control: cache_control) {"Cannot cache response with cache-control header!"} return false end end if set_cookie = headers[SET_COOKIE] Console.logger.debug(self) {"Cannot cache response with set-cookie header!"} return false end return true end
def call(request)
def call(request) cache_control = request.headers[CACHE_CONTROL] unless cache_control&.no_cache? key = self.key(request) if response = @store.lookup(key, request) Console.logger.debug(self, key: key) {"Cache hit!"} @count += 1 # Return the cached response: return response end end unless cache_control&.no_store? return wrap(key, request, super) end return super end
def close
def close @store.close ensure super end
def initialize(app, store: Store.default)
def initialize(app, store: Store.default) super(app) @count = 0 @store = store end
def key(request)
def key(request) @store.normalize(request) [request.authority, request.method, request.path] end
def proceed_with_response_cache?(response)
def proceed_with_response_cache?(response) if response.headers.trailer? unless cacheable_response_headers?(response.headers) Console.logger.debug(self, trailer: trailer.keys) {"Cannot cache response with trailer header!"} return false end end return true end
def wrap(key, request, response)
def wrap(key, request, response) if request.head? and body = response.body unless body.empty? Console.logger.warn(self) {"HEAD request resulted in non-empty body!"} return response end end unless cacheable_request?(request) Console.logger.debug(self) {"Cannot cache request!"} return response end unless cacheable_response?(response) Console.logger.debug(self) {"Cannot cache response!"} return response end return Body.wrap(response) do |response, body| if proceed_with_response_cache?(response) key ||= self.key(request) Console.logger.debug(self, key: key) {"Updating miss!"} @store.insert(key, request, Response.new(response, body)) end end end