faraday-http-cache 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +50 -10
- data/lib/faraday/http_cache/memory_store.rb +26 -0
- data/lib/faraday/http_cache/storage.rb +7 -186
- data/lib/faraday/http_cache/strategies/base_strategy.rb +93 -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 +11 -16
- data/spec/http_cache_spec.rb +1 -11
- data/spec/storage_spec.rb +18 -2
- 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 +138 -0
- metadata +24 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6603be3bbf6a2840ba11f947f7e42f030ab52600912e9a62d16aaf27ee5e6446
|
4
|
+
data.tar.gz: 95b8b2f4b08525406027d47e14341f5f301409a2518332b243dc08d2bde4e693
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4675c865c18641322c71b77881a74d3ca4edbf4fb1d628ac0fe7a6388d6a3c973946f1d7fa7b09d654f1eff61484a8c2d8524c966e4377c1e12854bf8114e13a
|
7
|
+
data.tar.gz: 832d7579eb75d3631e539ca7af996b9d062dbe43582468f98c349b3dbc963dde17cecf57a0202af1916297013143becba1a0fc1f32ddf92ab98490876e3efc84
|
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# Faraday Http Cache
|
2
2
|
|
3
|
-
[](https://rubygems.org/gems/faraday-http-cache)
|
4
|
+
[](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,194 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'digest/sha1'
|
3
|
+
require 'faraday/http_cache/strategies/by_url'
|
5
4
|
|
6
5
|
module Faraday
|
7
6
|
class HttpCache < Faraday::Middleware
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# Faraday::HttpCache::Storage.new(store: mem_cache_store)
|
15
|
-
#
|
16
|
-
# # Reuse some other instance of an ActiveSupport::Cache::Store object.
|
17
|
-
# Faraday::HttpCache::Storage.new(store: Rails.cache)
|
18
|
-
#
|
19
|
-
# # Creates a new Storage using Marshal for serialization.
|
20
|
-
# Faraday::HttpCache::Storage.new(store: Rails.cache, serializer: Marshal)
|
21
|
-
class Storage
|
22
|
-
# Public: Gets the underlying cache store object.
|
23
|
-
attr_reader :cache
|
24
|
-
|
25
|
-
# Internal: Initialize a new Storage object with a cache backend.
|
26
|
-
#
|
27
|
-
# :logger - A Logger object to be used to emit warnings.
|
28
|
-
# :store - An cache store object that should respond to 'read',
|
29
|
-
# 'write', and 'delete'.
|
30
|
-
# :serializer - A serializer object that should respond to 'dump'
|
31
|
-
# and 'load'.
|
32
|
-
def initialize(store: nil, serializer: nil, logger: nil)
|
33
|
-
@cache = store || MemoryStore.new
|
34
|
-
@serializer = serializer || JSON
|
35
|
-
@logger = logger
|
36
|
-
assert_valid_store!
|
37
|
-
end
|
38
|
-
|
39
|
-
# Internal: Store a response inside the cache.
|
40
|
-
#
|
41
|
-
# request - A Faraday::HttpCache::::Request instance of the executed HTTP
|
42
|
-
# request.
|
43
|
-
# response - The Faraday::HttpCache::Response instance to be stored.
|
44
|
-
#
|
45
|
-
# Returns nothing.
|
46
|
-
def write(request, response)
|
47
|
-
key = cache_key_for(request.url)
|
48
|
-
entry = serialize_entry(request.serializable_hash, response.serializable_hash)
|
49
|
-
|
50
|
-
entries = cache.read(key) || []
|
51
|
-
entries = entries.dup if entries.frozen?
|
52
|
-
|
53
|
-
entries.reject! do |(cached_request, cached_response)|
|
54
|
-
response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
|
55
|
-
end
|
56
|
-
|
57
|
-
entries << entry
|
58
|
-
|
59
|
-
cache.write(key, entries)
|
60
|
-
rescue ::Encoding::UndefinedConversionError => e
|
61
|
-
warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
|
62
|
-
raise e
|
63
|
-
end
|
64
|
-
|
65
|
-
# Internal: Attempt to retrieve an stored response that suits the incoming
|
66
|
-
# HTTP request.
|
67
|
-
#
|
68
|
-
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
69
|
-
# request.
|
70
|
-
# klass - The Class to be instantiated with the stored response.
|
71
|
-
#
|
72
|
-
# Returns an instance of 'klass'.
|
73
|
-
def read(request, klass: Faraday::HttpCache::Response)
|
74
|
-
cache_key = cache_key_for(request.url)
|
75
|
-
entries = cache.read(cache_key)
|
76
|
-
response = lookup_response(request, entries)
|
77
|
-
|
78
|
-
if response
|
79
|
-
klass.new(response)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def delete(url)
|
84
|
-
cache_key = cache_key_for(url)
|
85
|
-
cache.delete(cache_key)
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
# Internal: Retrieve a response Hash from the list of entries that match
|
91
|
-
# the given request.
|
92
|
-
#
|
93
|
-
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
94
|
-
# request.
|
95
|
-
# entries - An Array of pairs of Hashes (request, response).
|
96
|
-
#
|
97
|
-
# Returns a Hash or nil.
|
98
|
-
def lookup_response(request, entries)
|
99
|
-
if entries
|
100
|
-
entries = entries.map { |entry| deserialize_entry(*entry) }
|
101
|
-
_, response = entries.find { |req, res| response_matches?(request, req, res) }
|
102
|
-
response
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# Internal: Check if a cached response and request matches the given
|
107
|
-
# request.
|
108
|
-
#
|
109
|
-
# request - A Faraday::HttpCache::::Request instance of the
|
110
|
-
# current HTTP request.
|
111
|
-
# cached_request - The Hash of the request that was cached.
|
112
|
-
# cached_response - The Hash of the response that was cached.
|
113
|
-
#
|
114
|
-
# Returns true or false.
|
115
|
-
def response_matches?(request, cached_request, cached_response)
|
116
|
-
request.method.to_s == cached_request[:method].to_s &&
|
117
|
-
vary_matches?(cached_response, request, cached_request)
|
118
|
-
end
|
119
|
-
|
120
|
-
def vary_matches?(cached_response, request, cached_request)
|
121
|
-
headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
|
122
|
-
vary = headers['Vary'].to_s
|
123
|
-
|
124
|
-
vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
|
125
|
-
request.headers[header] == cached_request[:headers][header]
|
126
|
-
end)
|
127
|
-
end
|
128
|
-
|
129
|
-
def serialize_entry(*objects)
|
130
|
-
objects.map { |object| serialize_object(object) }
|
131
|
-
end
|
132
|
-
|
133
|
-
def serialize_object(object)
|
134
|
-
@serializer.dump(object)
|
135
|
-
end
|
136
|
-
|
137
|
-
def deserialize_entry(*objects)
|
138
|
-
objects.map { |object| deserialize_object(object) }
|
139
|
-
end
|
140
|
-
|
141
|
-
def deserialize_object(object)
|
142
|
-
@serializer.load(object).each_with_object({}) do |(key, value), hash|
|
143
|
-
hash[key.to_sym] = value
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
# Internal: Computes the cache key for a specific request, taking in
|
148
|
-
# account the current serializer to avoid cross serialization issues.
|
149
|
-
#
|
150
|
-
# url - The request URL.
|
151
|
-
#
|
152
|
-
# Returns a String.
|
153
|
-
def cache_key_for(url)
|
154
|
-
prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
|
155
|
-
Digest::SHA1.hexdigest("#{prefix}#{url}")
|
156
|
-
end
|
157
|
-
|
158
|
-
# Internal: Checks if the given cache object supports the
|
159
|
-
# expect API ('read' and 'write').
|
160
|
-
#
|
161
|
-
# Raises an 'ArgumentError'.
|
162
|
-
#
|
163
|
-
# Returns nothing.
|
164
|
-
def assert_valid_store!
|
165
|
-
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
|
166
|
-
raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def warn(message)
|
171
|
-
@logger&.warn(message)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
# Internal: A Hash based store to be used by the 'Storage' class
|
176
|
-
# when a 'store' is not provided for the middleware setup.
|
177
|
-
class MemoryStore
|
178
|
-
def initialize
|
179
|
-
@cache = {}
|
180
|
-
end
|
181
|
-
|
182
|
-
def read(key)
|
183
|
-
@cache[key]
|
184
|
-
end
|
185
|
-
|
186
|
-
def delete(key)
|
187
|
-
@cache.delete(key)
|
188
|
-
end
|
189
|
-
|
190
|
-
def write(key, value)
|
191
|
-
@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
|
192
13
|
end
|
193
14
|
end
|
194
15
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'faraday/http_cache/memory_store'
|
5
|
+
|
6
|
+
module Faraday
|
7
|
+
class HttpCache < Faraday::Middleware
|
8
|
+
module Strategies
|
9
|
+
# Base class for all strategies.
|
10
|
+
# @abstract
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
#
|
14
|
+
# # Creates a new strategy using a MemCached backend from ActiveSupport.
|
15
|
+
# mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
|
16
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: mem_cache_store)
|
17
|
+
#
|
18
|
+
# # Reuse some other instance of an ActiveSupport::Cache::Store object.
|
19
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache)
|
20
|
+
#
|
21
|
+
# # Creates a new strategy using Marshal for serialization.
|
22
|
+
# Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache, serializer: Marshal)
|
23
|
+
class BaseStrategy
|
24
|
+
# Returns the underlying cache store object.
|
25
|
+
attr_reader :cache
|
26
|
+
|
27
|
+
# @param [Hash] options the options to create a message with.
|
28
|
+
# @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
|
29
|
+
# store object that should respond to 'read', 'write', and 'delete'.
|
30
|
+
# @option options [#dump#load] :serializer - an object that should
|
31
|
+
# respond to 'dump' and 'load'.
|
32
|
+
# @option options [Logger, nil] :logger - an object to be used to emit warnings.
|
33
|
+
def initialize(options = {})
|
34
|
+
@cache = options[:store] || Faraday::HttpCache::MemoryStore.new
|
35
|
+
@serializer = options[:serializer] || JSON
|
36
|
+
@logger = options[:logger] || Logger.new(IO::NULL)
|
37
|
+
@cache_salt = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
|
38
|
+
assert_valid_store!
|
39
|
+
end
|
40
|
+
|
41
|
+
# Store a response inside the cache.
|
42
|
+
# @abstract
|
43
|
+
def write(_request, _response)
|
44
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Read a response from the cache.
|
48
|
+
# @abstract
|
49
|
+
def read(_request)
|
50
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
51
|
+
end
|
52
|
+
|
53
|
+
# Delete responses from the cache by the url.
|
54
|
+
# @abstract
|
55
|
+
def delete(_url)
|
56
|
+
raise NotImplementedError, 'Implement this method in your strategy'
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# @private
|
62
|
+
# @raise [ArgumentError] if the cache object doesn't support the expect API.
|
63
|
+
def assert_valid_store!
|
64
|
+
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
|
65
|
+
raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def serialize_entry(*objects)
|
70
|
+
objects.map { |object| serialize_object(object) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def serialize_object(object)
|
74
|
+
@serializer.dump(object)
|
75
|
+
end
|
76
|
+
|
77
|
+
def deserialize_entry(*objects)
|
78
|
+
objects.map { |object| deserialize_object(object) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def deserialize_object(object)
|
82
|
+
@serializer.load(object).each_with_object({}) do |(key, value), hash|
|
83
|
+
hash[key.to_sym] = value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def warn(message)
|
88
|
+
@logger.warn(message)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
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,]+/).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
|
data/lib/faraday/http_cache.rb
CHANGED
@@ -5,11 +5,12 @@ require 'faraday'
|
|
5
5
|
require 'faraday/http_cache/storage'
|
6
6
|
require 'faraday/http_cache/request'
|
7
7
|
require 'faraday/http_cache/response'
|
8
|
+
require 'faraday/http_cache/strategies'
|
8
9
|
|
9
10
|
module Faraday
|
10
11
|
# Public: The middleware responsible for caching and serving responses.
|
11
|
-
# The middleware use the provided configuration options to establish
|
12
|
-
# 'Faraday::HttpCache::
|
12
|
+
# The middleware use the provided configuration options to establish on of
|
13
|
+
# 'Faraday::HttpCache::Strategies' to cache responses retrieved by the stack
|
13
14
|
# adapter. If a stored response can be served again for a subsequent
|
14
15
|
# request, the middleware will return the response instead of issuing a new
|
15
16
|
# request to it's server. This middleware should be the last attached handler
|
@@ -91,7 +92,7 @@ module Faraday
|
|
91
92
|
# Faraday::HttpCache.new(app, logger: my_logger)
|
92
93
|
#
|
93
94
|
# # Initialize the middleware with a logger and Marshal as a serializer
|
94
|
-
# Faraday
|
95
|
+
# Faraday::HttpCache.new(app, logger: my_logger, serializer: Marshal)
|
95
96
|
#
|
96
97
|
# # Initialize the middleware with a FileStore at the 'tmp' dir.
|
97
98
|
# store = ActiveSupport::Cache.lookup_store(:file_store, ['tmp'])
|
@@ -104,17 +105,14 @@ module Faraday
|
|
104
105
|
super(app)
|
105
106
|
|
106
107
|
options = options.dup
|
107
|
-
@logger = options
|
108
|
+
@logger = options[:logger]
|
108
109
|
@shared_cache = options.delete(:shared_cache) { true }
|
109
110
|
@instrumenter = options.delete(:instrumenter)
|
110
111
|
@instrument_name = options.delete(:instrument_name) { EVENT_NAME }
|
111
112
|
|
112
|
-
|
113
|
-
serializer = options.delete(:serializer)
|
113
|
+
strategy = options.delete(:strategy) { Strategies::ByUrl }
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
@storage = Storage.new(store: store, serializer: serializer, logger: @logger)
|
115
|
+
@strategy = strategy.new(**options)
|
118
116
|
end
|
119
117
|
|
120
118
|
# Public: Process the request into a duplicate of this instance to
|
@@ -159,9 +157,6 @@ module Faraday
|
|
159
157
|
# Internal: Gets the request object created from the Faraday env Hash.
|
160
158
|
attr_reader :request
|
161
159
|
|
162
|
-
# Internal: Gets the storage instance associated with the middleware.
|
163
|
-
attr_reader :storage
|
164
|
-
|
165
160
|
private
|
166
161
|
|
167
162
|
# Internal: Should this cache instance act like a "shared cache" according
|
@@ -190,7 +185,7 @@ module Faraday
|
|
190
185
|
#
|
191
186
|
# Returns the 'Faraday::Response' instance to be served.
|
192
187
|
def process(env)
|
193
|
-
entry = @
|
188
|
+
entry = @strategy.read(@request)
|
194
189
|
|
195
190
|
return fetch(env) if entry.nil?
|
196
191
|
|
@@ -264,7 +259,7 @@ module Faraday
|
|
264
259
|
def store(response)
|
265
260
|
if shared_cache? ? response.cacheable_in_shared_cache? : response.cacheable_in_private_cache?
|
266
261
|
trace :store
|
267
|
-
@
|
262
|
+
@strategy.write(@request, response)
|
268
263
|
else
|
269
264
|
trace :uncacheable
|
270
265
|
end
|
@@ -274,10 +269,10 @@ module Faraday
|
|
274
269
|
headers = %w[Location Content-Location]
|
275
270
|
headers.each do |header|
|
276
271
|
url = response.headers[header]
|
277
|
-
@
|
272
|
+
@strategy.delete(url) if url
|
278
273
|
end
|
279
274
|
|
280
|
-
@
|
275
|
+
@strategy.delete(request.url)
|
281
276
|
trace :delete
|
282
277
|
end
|
283
278
|
|
data/spec/http_cache_spec.rb
CHANGED
@@ -283,23 +283,13 @@ describe Faraday::HttpCache do
|
|
283
283
|
expect(client.get('must-revalidate').body).to eq('1')
|
284
284
|
end
|
285
285
|
|
286
|
-
it 'raises an error when misconfigured' do
|
287
|
-
expect {
|
288
|
-
client = Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
|
289
|
-
stack.use Faraday::HttpCache, i_have_no_idea: true
|
290
|
-
end
|
291
|
-
|
292
|
-
client.get('get')
|
293
|
-
}.to raise_error(ArgumentError)
|
294
|
-
end
|
295
|
-
|
296
286
|
describe 'Configuration options' do
|
297
287
|
let(:app) { double('it is an app!') }
|
298
288
|
|
299
289
|
it 'uses the options to create a Cache Store' do
|
300
290
|
store = double(read: nil, write: nil)
|
301
291
|
|
302
|
-
expect(Faraday::HttpCache::
|
292
|
+
expect(Faraday::HttpCache::Strategies::ByUrl).to receive(:new).with(hash_including(store: store))
|
303
293
|
Faraday::HttpCache.new(app, store: store)
|
304
294
|
end
|
305
295
|
end
|
data/spec/storage_spec.rb
CHANGED
@@ -16,6 +16,21 @@ describe Faraday::HttpCache::Storage do
|
|
16
16
|
let(:storage) { Faraday::HttpCache::Storage.new(store: cache) }
|
17
17
|
subject { storage }
|
18
18
|
|
19
|
+
before do
|
20
|
+
allow(Kernel).to receive(:warn).with(
|
21
|
+
'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
|
22
|
+
'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates strategy and warns about deprecation' do
|
27
|
+
expect(Kernel).to receive(:warn).with(
|
28
|
+
'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
|
29
|
+
'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
|
30
|
+
)
|
31
|
+
is_expected.to be_a_kind_of(Faraday::HttpCache::Strategies::ByUrl)
|
32
|
+
end
|
33
|
+
|
19
34
|
describe 'Cache configuration' do
|
20
35
|
it 'uses a MemoryStore by default' do
|
21
36
|
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
@@ -128,9 +143,10 @@ describe Faraday::HttpCache::Storage do
|
|
128
143
|
end
|
129
144
|
|
130
145
|
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
146
|
+
current_time = Time.now
|
131
147
|
headers = {
|
132
|
-
'Date' => (
|
133
|
-
'Expires' => (
|
148
|
+
'Date' => (current_time - 39).httpdate,
|
149
|
+
'Expires' => (current_time + 40).httpdate
|
134
150
|
}
|
135
151
|
|
136
152
|
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::BaseStrategy do
|
6
|
+
subject(:strategy) { described_class.new }
|
7
|
+
|
8
|
+
it 'uses a MemoryStore as a default store' do
|
9
|
+
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
10
|
+
strategy
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when the given store is not valid' do
|
14
|
+
let(:store) { double(:wrong_store) }
|
15
|
+
subject(:strategy) { described_class.new(store: store) }
|
16
|
+
|
17
|
+
it 'raises an error' do
|
18
|
+
expect { strategy }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'raises an error when abstract methods are called' do
|
23
|
+
expect { strategy.write(nil, nil) }.to raise_error(NotImplementedError)
|
24
|
+
expect { strategy.read(nil) }.to raise_error(NotImplementedError)
|
25
|
+
expect { strategy.delete(nil) }.to raise_error(NotImplementedError)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::ByUrl do
|
6
|
+
let(:cache_key) { '6e3b941d0f7572291c777b3e48c04b74124a55d0' }
|
7
|
+
let(:request) do
|
8
|
+
env = { method: :get, url: 'http://test/index' }
|
9
|
+
double(env.merge(serializable_hash: env))
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:response) { double(serializable_hash: { response_headers: {} }) }
|
13
|
+
|
14
|
+
let(:cache) { Faraday::HttpCache::MemoryStore.new }
|
15
|
+
|
16
|
+
let(:strategy) { described_class.new(store: cache) }
|
17
|
+
subject { strategy }
|
18
|
+
|
19
|
+
describe 'Cache configuration' do
|
20
|
+
it 'uses a MemoryStore by default' do
|
21
|
+
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
22
|
+
described_class.new
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'raises an error when the given store is not valid' do
|
26
|
+
wrong = double
|
27
|
+
|
28
|
+
expect {
|
29
|
+
described_class.new(store: wrong)
|
30
|
+
}.to raise_error(ArgumentError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'storing responses' do
|
35
|
+
shared_examples 'A strategy with serialization' do
|
36
|
+
it 'writes the response object to the underlying cache' do
|
37
|
+
entry = [serializer.dump(request.serializable_hash), serializer.dump(response.serializable_hash)]
|
38
|
+
expect(cache).to receive(:write).with(cache_key, [entry])
|
39
|
+
subject.write(request, response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with the JSON serializer' do
|
44
|
+
let(:serializer) { JSON }
|
45
|
+
it_behaves_like 'A strategy with serialization'
|
46
|
+
|
47
|
+
context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
|
48
|
+
let(:response) do
|
49
|
+
body = String.new("\u2665").force_encoding('ASCII-8BIT')
|
50
|
+
double(:response, serializable_hash: { 'body' => body })
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'raises and logs a warning' do
|
54
|
+
logger = double(:logger, warn: nil)
|
55
|
+
strategy = described_class.new(logger: logger)
|
56
|
+
|
57
|
+
expect {
|
58
|
+
strategy.write(request, response)
|
59
|
+
}.to raise_error(::Encoding::UndefinedConversionError)
|
60
|
+
expect(logger).to have_received(:warn).with(
|
61
|
+
'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with the Marshal serializer' do
|
68
|
+
let(:cache_key) { '337d1e9c6c92423dd1c48a23054139058f97be40' }
|
69
|
+
let(:serializer) { Marshal }
|
70
|
+
let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
|
71
|
+
|
72
|
+
it_behaves_like 'A strategy with serialization'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'reading responses' do
|
77
|
+
let(:strategy) { described_class.new(store: cache, serializer: serializer) }
|
78
|
+
|
79
|
+
shared_examples 'A strategy with serialization' do
|
80
|
+
it 'returns nil if the response is not cached' do
|
81
|
+
expect(subject.read(request)).to be_nil
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'decodes a stored response' do
|
85
|
+
subject.write(request, response)
|
86
|
+
|
87
|
+
expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with the JSON serializer' do
|
92
|
+
let(:serializer) { JSON }
|
93
|
+
|
94
|
+
it_behaves_like 'A strategy with serialization'
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with the Marshal serializer' do
|
98
|
+
let(:serializer) { Marshal }
|
99
|
+
|
100
|
+
it_behaves_like 'A strategy with serialization'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'deleting responses' do
|
105
|
+
it 'removes the entries from the cache of the given URL' do
|
106
|
+
subject.write(request, response)
|
107
|
+
subject.delete(request.url)
|
108
|
+
expect(subject.read(request)).to be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
113
|
+
it 'is fresh if the response still has some time to live' do
|
114
|
+
headers = {
|
115
|
+
'Age' => 6,
|
116
|
+
'Cache-Control' => 'public, max-age=40',
|
117
|
+
'Date' => (Time.now - 38).httpdate,
|
118
|
+
'Expires' => (Time.now - 37).httpdate,
|
119
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
120
|
+
}
|
121
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
122
|
+
expect(response).to be_fresh
|
123
|
+
subject.write(request, response)
|
124
|
+
|
125
|
+
cached_response = subject.read(request)
|
126
|
+
expect(cached_response.max_age).to eq(34)
|
127
|
+
expect(cached_response).not_to be_fresh
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
131
|
+
headers = {
|
132
|
+
'Date' => (Time.now - 39).httpdate,
|
133
|
+
'Expires' => (Time.now + 40).httpdate
|
134
|
+
}
|
135
|
+
|
136
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
137
|
+
expect(response).to be_fresh
|
138
|
+
subject.write(request, response)
|
139
|
+
|
140
|
+
sleep(1)
|
141
|
+
cached_response = subject.read(request)
|
142
|
+
expect(cached_response).not_to be_fresh
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::ByVary do
|
6
|
+
let(:vary_index_cache_key) { '64896419583e8022efeb21d0ece6e266c0e58b59' }
|
7
|
+
let(:cache_key) { '25230d75622fffc4f4de8a6af69e6e3764f7eb6f' }
|
8
|
+
let(:vary) { '' }
|
9
|
+
let(:request) do
|
10
|
+
env = {method: :get, url: 'http://test/index'}
|
11
|
+
double(env.merge(serializable_hash: env))
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:response_payload) { {response_headers: {'Vary' => vary}} }
|
15
|
+
|
16
|
+
let(:response) do
|
17
|
+
instance_double(Faraday::HttpCache::Response, payload: response_payload, serializable_hash: response_payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:cache) { Faraday::HttpCache::MemoryStore.new }
|
21
|
+
|
22
|
+
let(:strategy) { described_class.new(store: cache) }
|
23
|
+
subject { strategy }
|
24
|
+
|
25
|
+
describe 'storing responses' do
|
26
|
+
shared_examples 'A strategy with serialization' do
|
27
|
+
it 'writes the response object to the underlying cache' do
|
28
|
+
entry = serializer.dump(response.serializable_hash)
|
29
|
+
expect(cache).to receive(:write).with(vary_index_cache_key, vary)
|
30
|
+
expect(cache).to receive(:write).with(cache_key, entry)
|
31
|
+
subject.write(request, response)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with the JSON serializer' do
|
36
|
+
let(:serializer) { JSON }
|
37
|
+
it_behaves_like 'A strategy with serialization'
|
38
|
+
|
39
|
+
context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
|
40
|
+
let(:response_payload) do
|
41
|
+
body = String.new("\u2665").force_encoding('ASCII-8BIT')
|
42
|
+
super().merge('body' => body)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'raises and logs a warning' do
|
46
|
+
logger = double(:logger, warn: nil)
|
47
|
+
strategy = described_class.new(logger: logger)
|
48
|
+
|
49
|
+
expect {
|
50
|
+
strategy.write(request, response)
|
51
|
+
}.to raise_error(::Encoding::UndefinedConversionError)
|
52
|
+
expect(logger).to have_received(:warn).with(
|
53
|
+
'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with the Marshal serializer' do
|
60
|
+
let(:vary_index_cache_key) { '6a7cb42440c10ef6edeb1826086a4d90b04103f0' }
|
61
|
+
let(:cache_key) { '45e0efd1a60d29ed69d6c6018dfcb96f58db89e0' }
|
62
|
+
let(:serializer) { Marshal }
|
63
|
+
let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
|
64
|
+
|
65
|
+
it_behaves_like 'A strategy with serialization'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'reading responses' do
|
70
|
+
let(:strategy) { described_class.new(store: cache, serializer: serializer) }
|
71
|
+
|
72
|
+
shared_examples 'A strategy with serialization' do
|
73
|
+
it 'returns nil if the response is not cached' do
|
74
|
+
expect(subject.read(request)).to be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'decodes a stored response' do
|
78
|
+
subject.write(request, response)
|
79
|
+
|
80
|
+
expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with the JSON serializer' do
|
85
|
+
let(:serializer) { JSON }
|
86
|
+
|
87
|
+
it_behaves_like 'A strategy with serialization'
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'with the Marshal serializer' do
|
91
|
+
let(:serializer) { Marshal }
|
92
|
+
|
93
|
+
it_behaves_like 'A strategy with serialization'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'deleting responses' do
|
98
|
+
it 'ignores delete method' do
|
99
|
+
subject.write(request, response)
|
100
|
+
subject.delete(request.url)
|
101
|
+
expect(subject.read(request)).not_to be_nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
106
|
+
it 'is fresh if the response still has some time to live' do
|
107
|
+
headers = {
|
108
|
+
'Age' => 6,
|
109
|
+
'Cache-Control' => 'public, max-age=40',
|
110
|
+
'Date' => (Time.now - 38).httpdate,
|
111
|
+
'Expires' => (Time.now - 37).httpdate,
|
112
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
113
|
+
}
|
114
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
115
|
+
expect(response).to be_fresh
|
116
|
+
subject.write(request, response)
|
117
|
+
|
118
|
+
cached_response = subject.read(request)
|
119
|
+
expect(cached_response.max_age).to eq(34)
|
120
|
+
expect(cached_response).not_to be_fresh
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
124
|
+
headers = {
|
125
|
+
'Date' => (Time.now - 39).httpdate,
|
126
|
+
'Expires' => (Time.now + 40).httpdate
|
127
|
+
}
|
128
|
+
|
129
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
130
|
+
expect(response).to be_fresh
|
131
|
+
subject.write(request, response)
|
132
|
+
|
133
|
+
sleep(1)
|
134
|
+
cached_response = subject.read(request)
|
135
|
+
expect(cached_response).not_to be_fresh
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faraday-http-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
+
- Lucas Mazza
|
7
8
|
- George Guimarães
|
8
9
|
- Gustavo Araujo
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2022-
|
13
|
+
date: 2022-06-08 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: faraday
|
@@ -37,9 +38,14 @@ files:
|
|
37
38
|
- lib/faraday-http-cache.rb
|
38
39
|
- lib/faraday/http_cache.rb
|
39
40
|
- lib/faraday/http_cache/cache_control.rb
|
41
|
+
- lib/faraday/http_cache/memory_store.rb
|
40
42
|
- lib/faraday/http_cache/request.rb
|
41
43
|
- lib/faraday/http_cache/response.rb
|
42
44
|
- lib/faraday/http_cache/storage.rb
|
45
|
+
- lib/faraday/http_cache/strategies.rb
|
46
|
+
- lib/faraday/http_cache/strategies/base_strategy.rb
|
47
|
+
- lib/faraday/http_cache/strategies/by_url.rb
|
48
|
+
- lib/faraday/http_cache/strategies/by_vary.rb
|
43
49
|
- spec/binary_spec.rb
|
44
50
|
- spec/cache_control_spec.rb
|
45
51
|
- spec/http_cache_spec.rb
|
@@ -49,6 +55,9 @@ files:
|
|
49
55
|
- spec/response_spec.rb
|
50
56
|
- spec/spec_helper.rb
|
51
57
|
- spec/storage_spec.rb
|
58
|
+
- spec/strategies/base_strategy_spec.rb
|
59
|
+
- spec/strategies/by_url_spec.rb
|
60
|
+
- spec/strategies/by_vary_spec.rb
|
52
61
|
- spec/support/empty.png
|
53
62
|
- spec/support/test_app.rb
|
54
63
|
- spec/support/test_server.rb
|
@@ -72,21 +81,24 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
81
|
- !ruby/object:Gem::Version
|
73
82
|
version: '0'
|
74
83
|
requirements: []
|
75
|
-
rubygems_version: 3.
|
84
|
+
rubygems_version: 3.2.3
|
76
85
|
signing_key:
|
77
86
|
specification_version: 4
|
78
87
|
summary: A Faraday middleware that stores and validates cache expiration.
|
79
88
|
test_files:
|
80
|
-
- spec/
|
81
|
-
- spec/
|
82
|
-
- spec/
|
89
|
+
- spec/binary_spec.rb
|
90
|
+
- spec/cache_control_spec.rb
|
91
|
+
- spec/http_cache_spec.rb
|
83
92
|
- spec/instrumentation_spec.rb
|
93
|
+
- spec/json_spec.rb
|
94
|
+
- spec/request_spec.rb
|
95
|
+
- spec/response_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
84
97
|
- spec/storage_spec.rb
|
85
|
-
- spec/
|
86
|
-
- spec/
|
87
|
-
- spec/
|
98
|
+
- spec/strategies/base_strategy_spec.rb
|
99
|
+
- spec/strategies/by_url_spec.rb
|
100
|
+
- spec/strategies/by_vary_spec.rb
|
88
101
|
- spec/support/empty.png
|
102
|
+
- spec/support/test_app.rb
|
89
103
|
- spec/support/test_server.rb
|
90
|
-
- spec/
|
91
|
-
- spec/cache_control_spec.rb
|
92
|
-
- spec/response_spec.rb
|
104
|
+
- spec/validation_spec.rb
|