danbooru::http: factor out cache feature.

Fixes a bug with cookies stored by the `session` feature not being sent
with cached requests.
This commit is contained in:
evazion
2020-06-21 18:27:32 -05:00
parent f85eef9bcd
commit bd25be95f5
3 changed files with 64 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
require "danbooru/http/html_adapter"
require "danbooru/http/xml_adapter"
require "danbooru/http/cache"
require "danbooru/http/redirector"
require "danbooru/http/retriable"
require "danbooru/http/session"
@@ -12,7 +13,7 @@ module Danbooru
DEFAULT_TIMEOUT = 10
MAX_REDIRECTS = 5
attr_accessor :cache, :max_size, :http
attr_accessor :max_size, :http
class << self
delegate :get, :head, :put, :post, :delete, :cache, :follow, :max_size, :timeout, :auth, :basic_auth, :headers, :cookies, :use, :public_only, :download_media, to: :new
@@ -49,10 +50,6 @@ module Danbooru
request(:delete, url, **options)
end
def cache(expiry)
dup.tap { |o| o.cache = expiry.to_i }
end
def follow(*args)
dup.tap { |o| o.http = o.http.follow(*args) }
end
@@ -85,6 +82,10 @@ module Danbooru
dup.tap { |o| o.http = o.http.use(*args) }
end
def cache(expires_in)
use(cache: { expires_in: expires_in })
end
# allow requests only to public IPs, not to local or private networks.
def public_only
dup.tap do |o|
@@ -128,11 +129,7 @@ module Danbooru
protected
def request(method, url, **options)
if @cache.present?
cached_request(method, url, **options)
else
raw_request(method, url, **options)
end
http.send(method, url, **options)
rescue ValidatingSocket::ProhibitedIpError
fake_response(597, "")
rescue HTTP::Redirector::TooManyRedirectsError
@@ -141,21 +138,6 @@ module Danbooru
fake_response(599, "")
end
def cached_request(method, url, **options)
key = Cache.hash({ method: method, url: url, headers: http.default_options.headers.to_h, **options }.to_json)
cached_response = Cache.get(key, @cache) do
response = raw_request(method, url, **options)
{ status: response.status, body: response.to_s, headers: response.headers.to_h, version: "1.1" }
end
::HTTP::Response.new(**cached_response)
end
def raw_request(method, url, **options)
http.send(method, url, **options)
end
def fake_response(status, body)
::HTTP::Response.new(status: status, version: "1.1", body: ::HTTP::Response::Body.new(body))
end

View File

@@ -0,0 +1,30 @@
module Danbooru
class Http
class Cache < HTTP::Feature
HTTP::Options.register_feature :cache, self
attr_reader :expires_in
def initialize(expires_in:)
@expires_in = expires_in
end
def perform(request, &block)
::Cache.get(cache_key(request), expires_in) do
response = yield request
# XXX hack to remove connection state from response body so we can serialize it for caching.
response.flush
response.body.instance_variable_set(:@connection, nil)
response.body.instance_variable_set(:@stream, nil)
response
end
end
def cache_key(request)
"http:" + ::Cache.hash({ method: request.verb, url: request.uri.to_s, headers: request.headers.sort }.to_json)
end
end
end
end

View File

@@ -62,14 +62,37 @@ class DanbooruHttpTest < ActiveSupport::TestCase
resp4 = http.cookies(def: 3, ghi: 4).get("https://httpbin.org/cookies")
assert_equal({ abc: "1", def: "3", ghi: "4" }, resp4.parse["cookies"].symbolize_keys)
end
end
should "cache requests" do
response1 = Danbooru::Http.cache(1.minute).get("https://httpbin.org/uuid")
context "cache feature" do
should "cache multiple requests to the same url" do
http = Danbooru::Http.cache(1.hour)
response1 = http.get("https://httpbin.org/uuid")
assert_equal(200, response1.status)
response2 = Danbooru::Http.cache(1.minute).get("https://httpbin.org/uuid")
response2 = http.get("https://httpbin.org/uuid")
assert_equal(200, response2.status)
assert_equal(response2.body, response1.body)
assert_equal(response2.to_s, response1.to_s)
end
should "cache cookies correctly" do
http = Danbooru::Http.cache(1.hour)
resp1 = http.get("https://httpbin.org/cookies")
resp2 = http.get("https://httpbin.org/cookies/set/abc/1")
resp3 = http.get("https://httpbin.org/cookies/set/def/2")
resp4 = http.get("https://httpbin.org/cookies")
assert_equal(200, resp1.status)
assert_equal(200, resp2.status)
assert_equal(200, resp3.status)
assert_equal(200, resp4.status)
assert_equal({}, resp1.parse["cookies"].symbolize_keys)
assert_equal({ abc: "1" }, resp2.parse["cookies"].symbolize_keys)
assert_equal({ abc: "1", def: "2" }, resp3.parse["cookies"].symbolize_keys)
assert_equal({ abc: "1", def: "2" }, resp4.parse["cookies"].symbolize_keys)
end
end