faraday-http-cache 2.2.0 → 2.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +50 -10
- data/lib/faraday/http_cache/cache_control.rb +1 -0
- data/lib/faraday/http_cache/memory_store.rb +26 -0
- data/lib/faraday/http_cache/request.rb +2 -0
- data/lib/faraday/http_cache/response.rb +2 -1
- data/lib/faraday/http_cache/storage.rb +8 -186
- data/lib/faraday/http_cache/strategies/base_strategy.rb +94 -0
- data/lib/faraday/http_cache/strategies/by_url.rb +118 -0
- data/lib/faraday/http_cache/strategies/by_vary.rb +86 -0
- data/lib/faraday/http_cache/strategies.rb +4 -0
- data/lib/faraday/http_cache.rb +22 -19
- data/spec/binary_spec.rb +2 -1
- data/spec/cache_control_spec.rb +1 -0
- data/spec/http_cache_spec.rb +3 -12
- data/spec/instrumentation_spec.rb +1 -0
- data/spec/json_spec.rb +1 -0
- data/spec/request_spec.rb +1 -0
- data/spec/response_spec.rb +6 -5
- data/spec/spec_helper.rb +8 -3
- data/spec/storage_spec.rb +25 -8
- data/spec/strategies/base_strategy_spec.rb +27 -0
- data/spec/strategies/by_url_spec.rb +145 -0
- data/spec/strategies/by_vary_spec.rb +145 -0
- data/spec/support/test_app.rb +2 -1
- data/spec/support/test_server.rb +8 -6
- data/spec/validation_spec.rb +1 -0
- metadata +29 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f9734c5eda496aee6fc6d189e27f9e39b43600ffb467976649ad3f7046a8efc
|
4
|
+
data.tar.gz: 1a4d2d137c3ec0a97790cc4b5bcb706f7e0c2af0d0700e0ae03dfc79f5e2bdba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f93240786d8c3b8012cce14545e2f0f34f1bb12811da4edc549ad145284712b59380dac02f91ed6170d19af7af220e48d905a31a1159129d2b32b45799251df6
|
7
|
+
data.tar.gz: 8b2480bebc178003743ca2e0535966ad617349ef7ead2f7b5907f00d4580ba990332472d2ac393dc6b5bd79b787d8d560e5d772f9bfc1faca76ac6b34a5061d7
|
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# Faraday Http Cache
|
2
2
|
|
3
|
-
[![
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/faraday-http-cache.svg)](https://rubygems.org/gems/faraday-http-cache)
|
4
|
+
[![Build](https://github.com/sourcelevel/faraday-http-cache/actions/workflows/main.yml/badge.svg)](https://github.com/sourcelevel/faraday-http-cache/actions)
|
4
5
|
|
5
|
-
|
6
|
+
A [Faraday](https://github.com/lostisland/faraday) middleware that respects HTTP cache,
|
6
7
|
by checking expiration and validation of the stored responses.
|
7
8
|
|
8
9
|
## Installation
|
@@ -53,15 +54,15 @@ This type of store **might not be persisted across multiple processes or connect
|
|
53
54
|
so it is probably not suitable for most production environments.
|
54
55
|
Make sure that you configure a store that is suitable for you.
|
55
56
|
|
56
|
-
The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
|
57
|
-
characters in responses. For example, if your JSON returns `"name": "Raül"` then
|
58
|
-
errors like:
|
57
|
+
The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
|
58
|
+
characters in responses in Ruby < 3.1. For example, if your JSON returns `"name": "Raül"` then
|
59
|
+
you might see errors like:
|
59
60
|
|
60
61
|
```
|
61
62
|
Response could not be serialized: "\xC3" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.
|
62
63
|
```
|
63
64
|
|
64
|
-
For full unicode support, or if you expect to be dealing with images, you can use
|
65
|
+
For full unicode support, or if you expect to be dealing with images, you can use the stdlib
|
65
66
|
[Marshal][marshal] instead. Alternatively you could use another json library like `oj` or `yajl-ruby`.
|
66
67
|
|
67
68
|
```ruby
|
@@ -71,9 +72,47 @@ client = Faraday.new do |builder|
|
|
71
72
|
end
|
72
73
|
```
|
73
74
|
|
75
|
+
### Strategies
|
76
|
+
|
77
|
+
You can provide a `:strategy` option to the middleware to specify the strategy to use.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
client = Faraday.new do |builder|
|
81
|
+
builder.use :http_cache, store: Rails.cache, strategy: Faraday::HttpCache::Strategies::ByVary
|
82
|
+
builder.adapter Faraday.default_adapter
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
Available strategies are:
|
87
|
+
|
88
|
+
#### `Faraday::HttpCache::Strategies::ByUrl`
|
89
|
+
|
90
|
+
The default strategy.
|
91
|
+
It Uses URL + HTTP method to generate cache keys and stores an array of request + response for each key.
|
92
|
+
|
93
|
+
#### `Faraday::HttpCache::Strategies::ByVary`
|
94
|
+
|
95
|
+
This strategy uses headers from `Vary` header to generate cache keys.
|
96
|
+
It also uses cache to store `Vary` headers mapped to the request URL.
|
97
|
+
This strategy is more suitable for caching private responses with the same URLs but different results for different users, like `https://api.github.com/user`.
|
98
|
+
|
99
|
+
*Note:* To automatically remove stale cache keys, you might want to use the `:expires_in` option.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
store = ActiveSupport::Cache.lookup_store(:redis_cache_store, expires_in: 1.day, url: 'redis://localhost:6379/0')
|
103
|
+
client = Faraday.new do |builder|
|
104
|
+
builder.use :http_cache, store: store, strategy: Faraday::HttpCache::Strategies::ByVary
|
105
|
+
builder.adapter Faraday.default_adapter
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
#### Custom strategies
|
110
|
+
|
111
|
+
You can write your own strategy by subclassing `Faraday::HttpCache::Strategies::BaseStrategy` and implementing `#write`, `#read` and `#delete` methods.
|
112
|
+
|
74
113
|
### Logging
|
75
114
|
|
76
|
-
You can provide a `:logger` option that will
|
115
|
+
You can provide a `:logger` option that will receive debug information based on the middleware
|
77
116
|
operations:
|
78
117
|
|
79
118
|
```ruby
|
@@ -82,7 +121,7 @@ client = Faraday.new do |builder|
|
|
82
121
|
builder.adapter Faraday.default_adapter
|
83
122
|
end
|
84
123
|
|
85
|
-
client.get('
|
124
|
+
client.get('https://site/api/users')
|
86
125
|
# logs "HTTP Cache: [GET users] miss, store"
|
87
126
|
```
|
88
127
|
|
@@ -133,6 +172,7 @@ execute the files under the `examples` directory to see a sample of the middlewa
|
|
133
172
|
## What gets cached?
|
134
173
|
|
135
174
|
The middleware will use the following headers to make caching decisions:
|
175
|
+
- Vary
|
136
176
|
- Cache-Control
|
137
177
|
- Age
|
138
178
|
- Last-Modified
|
@@ -155,7 +195,7 @@ client = Faraday.new do |builder|
|
|
155
195
|
builder.adapter Faraday.default_adapter
|
156
196
|
end
|
157
197
|
|
158
|
-
client.get('
|
198
|
+
client.get('https://site/api/some-private-resource') # => will be cached
|
159
199
|
```
|
160
200
|
|
161
201
|
## License
|
@@ -163,4 +203,4 @@ client.get('http://site/api/some-private-resource') # => will be cached
|
|
163
203
|
Copyright (c) 2012-2018 Plataformatec.
|
164
204
|
Copyright (c) 2019 SourceLevel and contributors.
|
165
205
|
|
166
|
-
[marshal]:
|
206
|
+
[marshal]: https://www.ruby-doc.org/core-3.0/Marshal.html
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faraday
|
4
|
+
class HttpCache < Faraday::Middleware
|
5
|
+
# @private
|
6
|
+
# A Hash based store to be used by strategies
|
7
|
+
# when a `store` is not provided for the middleware setup.
|
8
|
+
class MemoryStore
|
9
|
+
def initialize
|
10
|
+
@cache = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(key)
|
14
|
+
@cache[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(key)
|
18
|
+
@cache.delete(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(key, value)
|
22
|
+
@cache[key] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Faraday
|
3
4
|
class HttpCache < Faraday::Middleware
|
4
5
|
# Internal: A class to represent a request
|
@@ -24,6 +25,7 @@ module Faraday
|
|
24
25
|
def cacheable?
|
25
26
|
return false if method != :get && method != :head
|
26
27
|
return false if cache_control.no_store?
|
28
|
+
|
27
29
|
true
|
28
30
|
end
|
29
31
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'time'
|
3
4
|
require 'faraday/http_cache/cache_control'
|
4
5
|
|
@@ -204,7 +205,7 @@ module Faraday
|
|
204
205
|
# Returns nothing.
|
205
206
|
def ensure_date_header!
|
206
207
|
date
|
207
|
-
rescue
|
208
|
+
rescue StandardError
|
208
209
|
headers['Date'] = @now.httpdate
|
209
210
|
end
|
210
211
|
|
@@ -1,193 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require '
|
2
|
+
|
3
|
+
require 'faraday/http_cache/strategies/by_url'
|
4
4
|
|
5
5
|
module Faraday
|
6
6
|
class HttpCache < Faraday::Middleware
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# Faraday::HttpCache::Storage.new(store: mem_cache_store)
|
14
|
-
#
|
15
|
-
# # Reuse some other instance of an ActiveSupport::Cache::Store object.
|
16
|
-
# Faraday::HttpCache::Storage.new(store: Rails.cache)
|
17
|
-
#
|
18
|
-
# # Creates a new Storage using Marshal for serialization.
|
19
|
-
# Faraday::HttpCache::Storage.new(store: Rails.cache, serializer: Marshal)
|
20
|
-
class Storage
|
21
|
-
# Public: Gets the underlying cache store object.
|
22
|
-
attr_reader :cache
|
23
|
-
|
24
|
-
# Internal: Initialize a new Storage object with a cache backend.
|
25
|
-
#
|
26
|
-
# :logger - A Logger object to be used to emit warnings.
|
27
|
-
# :store - An cache store object that should respond to 'read',
|
28
|
-
# 'write', and 'delete'.
|
29
|
-
# :serializer - A serializer object that should respond to 'dump'
|
30
|
-
# and 'load'.
|
31
|
-
def initialize(store: nil, serializer: nil, logger: nil)
|
32
|
-
@cache = store || MemoryStore.new
|
33
|
-
@serializer = serializer || JSON
|
34
|
-
@logger = logger
|
35
|
-
assert_valid_store!
|
36
|
-
end
|
37
|
-
|
38
|
-
# Internal: Store a response inside the cache.
|
39
|
-
#
|
40
|
-
# request - A Faraday::HttpCache::::Request instance of the executed HTTP
|
41
|
-
# request.
|
42
|
-
# response - The Faraday::HttpCache::Response instance to be stored.
|
43
|
-
#
|
44
|
-
# Returns nothing.
|
45
|
-
def write(request, response)
|
46
|
-
key = cache_key_for(request.url)
|
47
|
-
entry = serialize_entry(request.serializable_hash, response.serializable_hash)
|
48
|
-
|
49
|
-
entries = cache.read(key) || []
|
50
|
-
entries = entries.dup if entries.frozen?
|
51
|
-
|
52
|
-
entries.reject! do |(cached_request, cached_response)|
|
53
|
-
response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
|
54
|
-
end
|
55
|
-
|
56
|
-
entries << entry
|
57
|
-
|
58
|
-
cache.write(key, entries)
|
59
|
-
rescue ::Encoding::UndefinedConversionError => e
|
60
|
-
warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
|
61
|
-
raise e
|
62
|
-
end
|
63
|
-
|
64
|
-
# Internal: Attempt to retrieve an stored response that suits the incoming
|
65
|
-
# HTTP request.
|
66
|
-
#
|
67
|
-
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
68
|
-
# request.
|
69
|
-
# klass - The Class to be instantiated with the stored response.
|
70
|
-
#
|
71
|
-
# Returns an instance of 'klass'.
|
72
|
-
def read(request, klass: Faraday::HttpCache::Response)
|
73
|
-
cache_key = cache_key_for(request.url)
|
74
|
-
entries = cache.read(cache_key)
|
75
|
-
response = lookup_response(request, entries)
|
76
|
-
|
77
|
-
if response
|
78
|
-
klass.new(response)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def delete(url)
|
83
|
-
cache_key = cache_key_for(url)
|
84
|
-
cache.delete(cache_key)
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
# Internal: Retrieve a response Hash from the list of entries that match
|
90
|
-
# the given request.
|
91
|
-
#
|
92
|
-
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
93
|
-
# request.
|
94
|
-
# entries - An Array of pairs of Hashes (request, response).
|
95
|
-
#
|
96
|
-
# Returns a Hash or nil.
|
97
|
-
def lookup_response(request, entries)
|
98
|
-
if entries
|
99
|
-
entries = entries.map { |entry| deserialize_entry(*entry) }
|
100
|
-
_, response = entries.find { |req, res| response_matches?(request, req, res) }
|
101
|
-
response
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
# Internal: Check if a cached response and request matches the given
|
106
|
-
# request.
|
107
|
-
#
|
108
|
-
# request - A Faraday::HttpCache::::Request instance of the
|
109
|
-
# current HTTP request.
|
110
|
-
# cached_request - The Hash of the request that was cached.
|
111
|
-
# cached_response - The Hash of the response that was cached.
|
112
|
-
#
|
113
|
-
# Returns true or false.
|
114
|
-
def response_matches?(request, cached_request, cached_response)
|
115
|
-
request.method.to_s == cached_request[:method].to_s &&
|
116
|
-
vary_matches?(cached_response, request, cached_request)
|
117
|
-
end
|
118
|
-
|
119
|
-
def vary_matches?(cached_response, request, cached_request)
|
120
|
-
headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
|
121
|
-
vary = headers['Vary'].to_s
|
122
|
-
|
123
|
-
vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
|
124
|
-
request.headers[header] == cached_request[:headers][header]
|
125
|
-
end)
|
126
|
-
end
|
127
|
-
|
128
|
-
def serialize_entry(*objects)
|
129
|
-
objects.map { |object| serialize_object(object) }
|
130
|
-
end
|
131
|
-
|
132
|
-
def serialize_object(object)
|
133
|
-
@serializer.dump(object)
|
134
|
-
end
|
135
|
-
|
136
|
-
def deserialize_entry(*objects)
|
137
|
-
objects.map { |object| deserialize_object(object) }
|
138
|
-
end
|
139
|
-
|
140
|
-
def deserialize_object(object)
|
141
|
-
@serializer.load(object).each_with_object({}) do |(key, value), hash|
|
142
|
-
hash[key.to_sym] = value
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Internal: Computes the cache key for a specific request, taking in
|
147
|
-
# account the current serializer to avoid cross serialization issues.
|
148
|
-
#
|
149
|
-
# url - The request URL.
|
150
|
-
#
|
151
|
-
# Returns a String.
|
152
|
-
def cache_key_for(url)
|
153
|
-
prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
|
154
|
-
Digest::SHA1.hexdigest("#{prefix}#{url}")
|
155
|
-
end
|
156
|
-
|
157
|
-
# Internal: Checks if the given cache object supports the
|
158
|
-
# expect API ('read' and 'write').
|
159
|
-
#
|
160
|
-
# Raises an 'ArgumentError'.
|
161
|
-
#
|
162
|
-
# Returns nothing.
|
163
|
-
def assert_valid_store!
|
164
|
-
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
|
165
|
-
raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def warn(message)
|
170
|
-
@logger.warn(message) if @logger
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
# Internal: A Hash based store to be used by the 'Storage' class
|
175
|
-
# when a 'store' is not provided for the middleware setup.
|
176
|
-
class MemoryStore
|
177
|
-
def initialize
|
178
|
-
@cache = {}
|
179
|
-
end
|
180
|
-
|
181
|
-
def read(key)
|
182
|
-
@cache[key]
|
183
|
-
end
|
184
|
-
|
185
|
-
def delete(key)
|
186
|
-
@cache.delete(key)
|
187
|
-
end
|
188
|
-
|
189
|
-
def write(key, value)
|
190
|
-
@cache[key] = value
|
7
|
+
# @deprecated Use Faraday::HttpCache::Strategies::ByUrl instead.
|
8
|
+
class Storage < Faraday::HttpCache::Strategies::ByUrl
|
9
|
+
def initialize(*)
|
10
|
+
Kernel.warn("Deprecated: #{self.class} is deprecated and will be removed in " \
|
11
|
+
'the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.')
|
12
|
+
super
|
191
13
|
end
|
192
14
|
end
|
193
15
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'faraday/http_cache/memory_store'
|
6
|
+
|
7
|
+
module Faraday
|
8
|
+
class HttpCache < Faraday::Middleware
|
9
|
+
module Strategies
|
10
|
+
# Base class for all strategies.
|
11
|
+
# @abstract
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# # Creates a new strategy using a MemCached backend from ActiveSupport.
|
16
|
+
# mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
|
17
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: mem_cache_store)
|
18
|
+
#
|
19
|
+
# # Reuse some other instance of an ActiveSupport::Cache::Store object.
|
20
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache)
|
21
|
+
#
|
22
|
+
# # Creates a new strategy using Marshal for serialization.
|
23
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache, serializer: Marshal)
|
24
|
+
class BaseStrategy
|
25
|
+
# Returns the underlying cache store object.
|
26
|
+
attr_reader :cache
|
27
|
+
|
28
|
+
# @param [Hash] options the options to create a message with.
|
29
|
+
# @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
|
30
|
+
# store object that should respond to 'read', 'write', and 'delete'.
|
31
|
+
# @option options [#dump#load] :serializer - an object that should
|
32
|
+
# respond to 'dump' and 'load'.
|
33
|
+
# @option options [Logger, nil] :logger - an object to be used to emit warnings.
|
34
|
+
def initialize(options = {})
|
35
|
+
@cache = options[:store] || Faraday::HttpCache::MemoryStore.new
|
36
|
+
@serializer = options[:serializer] || JSON
|
37
|
+
@logger = options[:logger] || Logger.new(IO::NULL)
|
38
|
+
@cache_salt = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
|
39
|
+
assert_valid_store!
|
40
|
+
end
|
41
|
+
|
42
|
+
# Store a response inside the cache.
|
43
|
+
# @abstract
|
44
|
+
def write(_request, _response)
|
45
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Read a response from the cache.
|
49
|
+
# @abstract
|
50
|
+
def read(_request)
|
51
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Delete responses from the cache by the url.
|
55
|
+
# @abstract
|
56
|
+
def delete(_url)
|
57
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# @private
|
63
|
+
# @raise [ArgumentError] if the cache object doesn't support the expect API.
|
64
|
+
def assert_valid_store!
|
65
|
+
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
|
66
|
+
raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def serialize_entry(*objects)
|
71
|
+
objects.map { |object| serialize_object(object) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def serialize_object(object)
|
75
|
+
@serializer.dump(object)
|
76
|
+
end
|
77
|
+
|
78
|
+
def deserialize_entry(*objects)
|
79
|
+
objects.map { |object| deserialize_object(object) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def deserialize_object(object)
|
83
|
+
@serializer.load(object).each_with_object({}) do |(key, value), hash|
|
84
|
+
hash[key.to_sym] = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def warn(message)
|
89
|
+
@logger.warn(message)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
require 'faraday/http_cache/strategies/base_strategy'
|
6
|
+
|
7
|
+
module Faraday
|
8
|
+
class HttpCache < Faraday::Middleware
|
9
|
+
module Strategies
|
10
|
+
# The original strategy by Faraday::HttpCache.
|
11
|
+
# Uses URL + HTTP method to generate cache keys.
|
12
|
+
class ByUrl < BaseStrategy
|
13
|
+
# Store a response inside the cache.
|
14
|
+
#
|
15
|
+
# @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
|
16
|
+
# @param [Faraday::HttpCache::Response] response - instance to be stored.
|
17
|
+
#
|
18
|
+
# @return [void]
|
19
|
+
def write(request, response)
|
20
|
+
key = cache_key_for(request.url)
|
21
|
+
entry = serialize_entry(request.serializable_hash, response.serializable_hash)
|
22
|
+
entries = cache.read(key) || []
|
23
|
+
entries = entries.dup if entries.frozen?
|
24
|
+
entries.reject! do |(cached_request, cached_response)|
|
25
|
+
response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
|
26
|
+
end
|
27
|
+
|
28
|
+
entries << entry
|
29
|
+
|
30
|
+
cache.write(key, entries)
|
31
|
+
rescue ::Encoding::UndefinedConversionError => e
|
32
|
+
warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
|
33
|
+
raise e
|
34
|
+
end
|
35
|
+
|
36
|
+
# Fetch a stored response that suits the incoming HTTP request or return nil.
|
37
|
+
#
|
38
|
+
# @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
|
39
|
+
#
|
40
|
+
# @return [Faraday::HttpCache::Response, nil]
|
41
|
+
def read(request)
|
42
|
+
cache_key = cache_key_for(request.url)
|
43
|
+
entries = cache.read(cache_key)
|
44
|
+
response = lookup_response(request, entries)
|
45
|
+
return nil unless response
|
46
|
+
|
47
|
+
Faraday::HttpCache::Response.new(response)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param [String] url – the url of a changed resource, will be used to invalidate the cache.
|
51
|
+
#
|
52
|
+
# @return [void]
|
53
|
+
def delete(url)
|
54
|
+
cache_key = cache_key_for(url)
|
55
|
+
cache.delete(cache_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Retrieve a response Hash from the list of entries that match the given request.
|
61
|
+
#
|
62
|
+
# @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
|
63
|
+
# @param [Array<Array(Hash, Hash)>] entries - pairs of Hashes (request, response).
|
64
|
+
#
|
65
|
+
# @return [Hash, nil]
|
66
|
+
def lookup_response(request, entries)
|
67
|
+
if entries
|
68
|
+
entries = entries.map { |entry| deserialize_entry(*entry) }
|
69
|
+
_, response = entries.find { |req, res| response_matches?(request, req, res) }
|
70
|
+
response
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Check if a cached response and request matches the given request.
|
75
|
+
#
|
76
|
+
# @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
|
77
|
+
# @param [Hash] cached_request - a Hash of the request that was cached.
|
78
|
+
# @param [Hash] cached_response - a Hash of the response that was cached.
|
79
|
+
#
|
80
|
+
# @return [true, false]
|
81
|
+
def response_matches?(request, cached_request, cached_response)
|
82
|
+
request.method.to_s == cached_request[:method].to_s &&
|
83
|
+
vary_matches?(cached_response, request, cached_request)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Check if the cached request matches the incoming
|
87
|
+
# request based on the Vary header of cached response.
|
88
|
+
#
|
89
|
+
# If Vary header is not present, the request is considered to match.
|
90
|
+
# If Vary header is '*', the request is considered to not match.
|
91
|
+
#
|
92
|
+
# @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
|
93
|
+
# @param [Hash] cached_request - a Hash of the request that was cached.
|
94
|
+
# @param [Hash] cached_response - a Hash of the response that was cached.
|
95
|
+
#
|
96
|
+
# @return [true, false]
|
97
|
+
def vary_matches?(cached_response, request, cached_request)
|
98
|
+
headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
|
99
|
+
vary = headers['Vary'].to_s
|
100
|
+
|
101
|
+
vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
|
102
|
+
request.headers[header] == cached_request[:headers][header]
|
103
|
+
end)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Computes the cache key for a specific request, taking
|
107
|
+
# in account the current serializer to avoid cross serialization issues.
|
108
|
+
#
|
109
|
+
# @param [String] url - the request URL.
|
110
|
+
#
|
111
|
+
# @return [String]
|
112
|
+
def cache_key_for(url)
|
113
|
+
Digest::SHA1.hexdigest("#{@cache_salt}#{url}")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
require 'faraday/http_cache/strategies/base_strategy'
|
6
|
+
|
7
|
+
module Faraday
|
8
|
+
class HttpCache < Faraday::Middleware
|
9
|
+
module Strategies
|
10
|
+
# This strategy uses headers from the Vary response header to generate cache keys.
|
11
|
+
# It also uses the index with Vary headers mapped to the request url.
|
12
|
+
# This strategy is more suitable for caching private responses with the same urls,
|
13
|
+
# like https://api.github.com/user.
|
14
|
+
#
|
15
|
+
# This strategy does not support #delete method to clear cache on unsafe methods.
|
16
|
+
class ByVary < BaseStrategy
|
17
|
+
# Store a response inside the cache.
|
18
|
+
#
|
19
|
+
# @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
|
20
|
+
# @param [Faraday::HttpCache::Response] response - instance to be stored.
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def write(request, response)
|
24
|
+
vary_cache_key = vary_cache_key_for(request)
|
25
|
+
headers = Faraday::Utils::Headers.new(response.payload[:response_headers])
|
26
|
+
vary = headers['Vary'].to_s
|
27
|
+
cache.write(vary_cache_key, vary)
|
28
|
+
|
29
|
+
response_cache_key = response_cache_key_for(request, vary)
|
30
|
+
entry = serialize_object(response.serializable_hash)
|
31
|
+
cache.write(response_cache_key, entry)
|
32
|
+
rescue ::Encoding::UndefinedConversionError => e
|
33
|
+
warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
|
34
|
+
raise e
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fetch a stored response that suits the incoming HTTP request or return nil.
|
38
|
+
#
|
39
|
+
# @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
|
40
|
+
#
|
41
|
+
# @return [Faraday::HttpCache::Response, nil]
|
42
|
+
def read(request)
|
43
|
+
vary_cache_key = vary_cache_key_for(request)
|
44
|
+
vary = cache.read(vary_cache_key)
|
45
|
+
return nil if vary.nil? || vary == '*'
|
46
|
+
|
47
|
+
cache_key = response_cache_key_for(request, vary)
|
48
|
+
response = cache.read(cache_key)
|
49
|
+
return nil if response.nil?
|
50
|
+
|
51
|
+
Faraday::HttpCache::Response.new(deserialize_object(response))
|
52
|
+
end
|
53
|
+
|
54
|
+
# This strategy does not support #delete method to clear cache on unsafe methods.
|
55
|
+
# @return [void]
|
56
|
+
def delete(_url)
|
57
|
+
# do nothing since we can't find the key by url
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Computes the cache key for the index with Vary headers.
|
63
|
+
#
|
64
|
+
# @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
def vary_cache_key_for(request)
|
68
|
+
method = request.method.to_s
|
69
|
+
Digest::SHA1.hexdigest("by_vary_index#{@cache_salt}#{method}#{request.url}")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Computes the cache key for the response.
|
73
|
+
#
|
74
|
+
# @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
|
75
|
+
# @param [String] vary - the Vary header value.
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
def response_cache_key_for(request, vary)
|
79
|
+
method = request.method.to_s
|
80
|
+
headers = vary.split(/[\s,]+/).uniq.sort.map { |header| request.headers[header] }
|
81
|
+
Digest::SHA1.hexdigest("by_vary#{@cache_salt}#{method}#{request.url}#{headers.join}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|