lib/httpx/plugins/response_cache.rb in httpx-0.20.3 vs lib/httpx/plugins/response_cache.rb in httpx-0.20.4
- old
+ new
@@ -7,26 +7,42 @@
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Response-Cache
#
module ResponseCache
CACHEABLE_VERBS = %i[get head].freeze
+ CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
private_constant :CACHEABLE_VERBS
+ private_constant :CACHEABLE_STATUS_CODES
class << self
def load_dependencies(*)
require_relative "response_cache/store"
end
def cacheable_request?(request)
- CACHEABLE_VERBS.include?(request.verb)
+ CACHEABLE_VERBS.include?(request.verb) &&
+ (
+ !request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
+ )
end
def cacheable_response?(response)
response.is_a?(Response) &&
- # partial responses shall not be cached, only full ones.
+ (
+ response.cache_control.nil? ||
+ # TODO: !response.cache_control.include?("private") && is shared cache
+ !response.cache_control.include?("no-store")
+ ) &&
+ CACHEABLE_STATUS_CODES.include?(response.status) &&
+ # RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
+ # 410 MAY be stored by a cache and used in reply to a subsequent
+ # request, subject to the expiration mechanism, unless a cache-control
+ # directive prohibits caching. However, a cache that does not support
+ # the Range and Content-Range headers MUST NOT cache 206 (Partial
+ # Content) responses.
response.status != 206 && (
- response.headers.key?("etag") || response.headers.key?("last-modified-at")
+ response.headers.key?("etag") || response.headers.key?("last-modified-at") || response.fresh?
)
end
def cached_response?(response)
response.is_a?(Response) && response.status == 304
@@ -50,37 +66,111 @@
@options.response_cache_store.clear
end
def build_request(*)
request = super
- return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request.uri)
+ return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request)
@options.response_cache_store.prepare(request)
request
end
def fetch_response(request, *)
response = super
- if response && ResponseCache.cached_response?(response)
+ return unless response
+
+ if ResponseCache.cached_response?(response)
log { "returning cached response for #{request.uri}" }
- cached_response = @options.response_cache_store.lookup(request.uri)
+ cached_response = @options.response_cache_store.lookup(request)
response.copy_from_cached(cached_response)
+
+ else
+ @options.response_cache_store.cache(request, response)
end
- @options.response_cache_store.cache(request.uri, response) if response && ResponseCache.cacheable_response?(response)
-
response
end
end
+ module RequestMethods
+ def response_cache_key
+ @response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}#{@uri}")
+ end
+ end
+
module ResponseMethods
def copy_from_cached(other)
@body = other.body
@body.__send__(:rewind)
+ end
+
+ # A response is fresh if its age has not yet exceeded its freshness lifetime.
+ def fresh?
+ if cache_control
+ return false if cache_control.include?("no-cache")
+
+ # check age: max-age
+ max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
+
+ max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
+
+ max_age = max_age[/age=(\d+)/, 1] if max_age
+
+ max_age = max_age.to_i if max_age
+
+ return max_age > age if max_age
+ end
+
+ # check age: expires
+ if @headers.key?("expires")
+ begin
+ expires = Time.httpdate(@headers["expires"])
+ rescue ArgumentError
+ return true
+ end
+
+ return (expires - Time.now).to_i.positive?
+ end
+
+ true
+ end
+
+ def cache_control
+ return @cache_control if defined?(@cache_control)
+
+ @cache_control = begin
+ return unless @headers.key?("cache-control")
+
+ @headers["cache-control"].split(/ *, */)
+ end
+ end
+
+ def vary
+ return @vary if defined?(@vary)
+
+ @vary = begin
+ return unless @headers.key?("vary")
+
+ @headers["vary"].split(/ *, */)
+ end
+ end
+
+ private
+
+ def age
+ return @headers["age"].to_i if @headers.key?("age")
+
+ (Time.now - date).to_i
+ end
+
+ def date
+ @date ||= Time.httpdate(@headers["date"])
+ rescue NoMethodError, ArgumentError
+ Time.now.httpdate
end
end
end
register_plugin :response_cache, ResponseCache
end