faraday-http-cache 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![
|
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,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
|