faraday-http-cache 2.6.1 → 2.8.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 +40 -7
- data/lib/faraday/http_cache/cache_control.rb +7 -0
- data/lib/faraday/http_cache/response.rb +35 -0
- data/lib/faraday/http_cache/strategies/base_strategy.rb +9 -2
- data/lib/faraday/http_cache/version.rb +7 -0
- data/lib/faraday/http_cache.rb +57 -3
- data/spec/cache_control_spec.rb +10 -0
- data/spec/http_cache_spec.rb +104 -0
- data/spec/instrumentation_spec.rb +10 -0
- data/spec/response_spec.rb +23 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/strategies/by_url_spec.rb +14 -0
- data/spec/strategies/by_vary_spec.rb +14 -0
- data/spec/support/json_gadget.rb +14 -0
- data/spec/support/test_app.rb +20 -0
- metadata +21 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00c9816f80d3f4a4f700bf153c6e4b20eed4f28fc0fcbc59d3e6a9135dd864fb
|
|
4
|
+
data.tar.gz: 1298dc1a38f6529db56b8d862524268908ac09a2825678450958955b2af8f096
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 959cd511c16d707e18369d66527bcd33dcec3de70e093f906cc688c6c68f715e2e74eefc0777e97eb00070d7edb906505b2edfd2e77e05778d66c42d33597d6b
|
|
7
|
+
data.tar.gz: 6acca89a4a35247611b553d293c627c053f7eba252ae2fa65ccc1fa07b85d5cedfb815e7924963fce288ecc113d98ca3c565426e604781aa0db2c83dd80482f6
|
data/README.md
CHANGED
|
@@ -62,8 +62,10 @@ you might see errors like:
|
|
|
62
62
|
Response could not be serialized: "\xC3" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
For full unicode support, or if you expect to be dealing with images, you can use
|
|
66
|
-
|
|
65
|
+
For full unicode support, or if you expect to be dealing with images, you can use another json
|
|
66
|
+
library like `oj` or `yajl-ruby`, or the stdlib [Marshal][marshal]. Only pick Marshal when you fully
|
|
67
|
+
trust the cache store: `Marshal.load` will instantiate any object found in the data, while the
|
|
68
|
+
default `JSON` serializer parses entries into plain hashes and never instantiates classes.
|
|
67
69
|
|
|
68
70
|
```ruby
|
|
69
71
|
client = Faraday.new do |builder|
|
|
@@ -72,6 +74,31 @@ client = Faraday.new do |builder|
|
|
|
72
74
|
end
|
|
73
75
|
```
|
|
74
76
|
|
|
77
|
+
### Stale-While-Revalidate and background refresh hooks
|
|
78
|
+
|
|
79
|
+
The middleware supports `stale-while-revalidate` directives from the `Cache-Control` header.
|
|
80
|
+
When a cached response is stale but still inside the `stale-while-revalidate` window, the middleware
|
|
81
|
+
will serve the stale response immediately.
|
|
82
|
+
|
|
83
|
+
You can provide an `:on_stale` callback to trigger your own asynchronous refresh logic:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
client = Faraday.new do |builder|
|
|
87
|
+
builder.use :http_cache,
|
|
88
|
+
store: Rails.cache,
|
|
89
|
+
on_stale: lambda { |request:, env:, cached_response:|
|
|
90
|
+
RefreshApiCacheJob.perform_later(request.url.to_s)
|
|
91
|
+
}
|
|
92
|
+
builder.adapter Faraday.default_adapter
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The callback receives:
|
|
97
|
+
|
|
98
|
+
- `request`: `Faraday::HttpCache::Request`
|
|
99
|
+
- `env`: current `Faraday::Env`
|
|
100
|
+
- `cached_response`: `Faraday::HttpCache::Response`
|
|
101
|
+
|
|
75
102
|
### Strategies
|
|
76
103
|
|
|
77
104
|
You can provide a `:strategy` option to the middleware to specify the strategy to use.
|
|
@@ -140,6 +167,8 @@ processes a request. In the event payload, `:env` contains the response Faraday
|
|
|
140
167
|
- `:valid` means that the cached response *could* be validated against the server.
|
|
141
168
|
- `:fresh` means that the cached response was still fresh and could be returned without even
|
|
142
169
|
calling the server.
|
|
170
|
+
- `:stale` means that the cached response was stale, but served while inside
|
|
171
|
+
`stale-while-revalidate` window.
|
|
143
172
|
|
|
144
173
|
```ruby
|
|
145
174
|
client = Faraday.new do |builder|
|
|
@@ -154,7 +183,7 @@ ActiveSupport::Notifications.subscribe "http_cache.faraday" do |*args|
|
|
|
154
183
|
statsd = Statsd.new
|
|
155
184
|
|
|
156
185
|
case cache_status
|
|
157
|
-
when :fresh, :valid
|
|
186
|
+
when :fresh, :valid, :stale
|
|
158
187
|
statsd.increment('api-calls.cache_hits')
|
|
159
188
|
when :invalid, :miss
|
|
160
189
|
statsd.increment('api-calls.cache_misses')
|
|
@@ -168,6 +197,7 @@ end
|
|
|
168
197
|
|
|
169
198
|
You can clone this repository, install its dependencies with Bundler (run `bundle install`) and
|
|
170
199
|
execute the files under the `examples` directory to see a sample of the middleware usage.
|
|
200
|
+
For stale-while-revalidate behavior with `:on_stale`, see `examples/stale_while_revalidate.rb`.
|
|
171
201
|
|
|
172
202
|
## What gets cached?
|
|
173
203
|
|
|
@@ -181,13 +211,16 @@ The middleware will use the following headers to make caching decisions:
|
|
|
181
211
|
|
|
182
212
|
### Cache-Control
|
|
183
213
|
|
|
184
|
-
The `max-age`, `must-revalidate`, `proxy-revalidate
|
|
214
|
+
The `max-age`, `must-revalidate`, `proxy-revalidate`, `s-maxage` and
|
|
215
|
+
`stale-while-revalidate` directives are checked.
|
|
185
216
|
|
|
186
217
|
### Shared vs. non-shared caches
|
|
187
218
|
|
|
188
|
-
By default, the middleware acts as a "shared cache" per RFC
|
|
189
|
-
responses with `Cache-Control: private
|
|
190
|
-
|
|
219
|
+
By default, the middleware acts as a "shared cache" per RFC 9111. This means it does not cache
|
|
220
|
+
responses with `Cache-Control: private`, and it only stores and reuses responses to requests that
|
|
221
|
+
carried an `Authorization` header when the response explicitly allows it with `public`,
|
|
222
|
+
`must-revalidate` or `s-maxage` (RFC 9111 section 3.5). This behavior can be changed by passing in
|
|
223
|
+
the `:shared_cache` configuration option:
|
|
191
224
|
|
|
192
225
|
```ruby
|
|
193
226
|
client = Faraday.new do |builder|
|
|
@@ -68,6 +68,13 @@ module Faraday
|
|
|
68
68
|
@directives['proxy-revalidate']
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Internal: Gets the 'stale-while-revalidate' directive as an Integer.
|
|
72
|
+
#
|
|
73
|
+
# Returns nil if the 'stale-while-revalidate' directive isn't present.
|
|
74
|
+
def stale_while_revalidate
|
|
75
|
+
@directives['stale-while-revalidate'].to_i if @directives.key?('stale-while-revalidate')
|
|
76
|
+
end
|
|
77
|
+
|
|
71
78
|
# Internal: Gets the String representation for the cache directives.
|
|
72
79
|
# Directives are joined by a '=' and then combined into a single String
|
|
73
80
|
# separated by commas. Directives with a 'true' value will omit the '='
|
|
@@ -54,6 +54,18 @@ module Faraday
|
|
|
54
54
|
!cache_control.no_cache? && ttl && ttl > 0
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Internal: Checks if the response is stale but can still be served while
|
|
58
|
+
# revalidating in the background.
|
|
59
|
+
#
|
|
60
|
+
# Returns true when the response has exceeded freshness lifetime, but is
|
|
61
|
+
# still inside the stale-while-revalidate window.
|
|
62
|
+
def stale_while_revalidate?
|
|
63
|
+
return false if cache_control.no_cache?
|
|
64
|
+
return false unless ttl && stale_while_revalidate
|
|
65
|
+
|
|
66
|
+
ttl <= 0 && -ttl <= stale_while_revalidate
|
|
67
|
+
end
|
|
68
|
+
|
|
57
69
|
# Internal: Checks if the Response returned a 'Not Modified' status.
|
|
58
70
|
#
|
|
59
71
|
# Returns true if the response status code is 304.
|
|
@@ -86,6 +98,22 @@ module Faraday
|
|
|
86
98
|
cacheable?(false)
|
|
87
99
|
end
|
|
88
100
|
|
|
101
|
+
# Internal: Checks if a shared cache may reuse this response for requests
|
|
102
|
+
# other than the one that carried an 'Authorization' header.
|
|
103
|
+
#
|
|
104
|
+
# RFC 9111 section 3.5: a shared cache must not use a cached response to
|
|
105
|
+
# a request with an 'Authorization' header to satisfy any subsequent
|
|
106
|
+
# request unless the response carries a 'Cache-Control' directive that
|
|
107
|
+
# explicitly allows it. The directives with that effect are
|
|
108
|
+
# 'must-revalidate', 'public' and 's-maxage'.
|
|
109
|
+
#
|
|
110
|
+
# Returns true if one of those directives is present.
|
|
111
|
+
def shared_cache_authorized?
|
|
112
|
+
cache_control.public? ||
|
|
113
|
+
cache_control.must_revalidate? ||
|
|
114
|
+
!cache_control.shared_max_age.nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
89
117
|
# Internal: Gets the response age in seconds.
|
|
90
118
|
#
|
|
91
119
|
# Returns the 'Age' header if present, or subtracts the response 'date'
|
|
@@ -123,6 +151,13 @@ module Faraday
|
|
|
123
151
|
(expires && (expires - @now))
|
|
124
152
|
end
|
|
125
153
|
|
|
154
|
+
# Internal: Gets the stale-while-revalidate value in seconds.
|
|
155
|
+
#
|
|
156
|
+
# Returns an Integer or nil.
|
|
157
|
+
def stale_while_revalidate
|
|
158
|
+
cache_control.stale_while_revalidate
|
|
159
|
+
end
|
|
160
|
+
|
|
126
161
|
# Internal: Creates a new 'Faraday::Response', merging the stored
|
|
127
162
|
# response with the supplied 'env' object.
|
|
128
163
|
#
|
|
@@ -29,7 +29,9 @@ module Faraday
|
|
|
29
29
|
# @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
|
|
30
30
|
# store object that should respond to 'read', 'write', and 'delete'.
|
|
31
31
|
# @option options [#dump#load] :serializer - an object that should
|
|
32
|
-
# respond to 'dump' and 'load'.
|
|
32
|
+
# respond to 'dump' and 'load'. 'load' must never instantiate classes
|
|
33
|
+
# named by the data, since the cached entries contain response headers
|
|
34
|
+
# sent by the origin server.
|
|
33
35
|
# @option options [Logger, nil] :logger - an object to be used to emit warnings.
|
|
34
36
|
def initialize(options = {})
|
|
35
37
|
@cache = options[:store] || Faraday::HttpCache::MemoryStore.new
|
|
@@ -80,7 +82,12 @@ module Faraday
|
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
def deserialize_object(object)
|
|
83
|
-
|
|
85
|
+
# JSON.load enables create_additions, so a `json_class` key in the
|
|
86
|
+
# entry would instantiate that class. Response headers are stored
|
|
87
|
+
# verbatim, which lets an origin server plant such a key. JSON.parse
|
|
88
|
+
# only ever builds plain Ruby objects.
|
|
89
|
+
loaded = @serializer.equal?(::JSON) ? ::JSON.parse(object) : @serializer.load(object)
|
|
90
|
+
loaded.transform_keys(&:to_sym)
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
def warn(message)
|
data/lib/faraday/http_cache.rb
CHANGED
|
@@ -6,6 +6,7 @@ require 'faraday/http_cache/storage'
|
|
|
6
6
|
require 'faraday/http_cache/request'
|
|
7
7
|
require 'faraday/http_cache/response'
|
|
8
8
|
require 'faraday/http_cache/strategies'
|
|
9
|
+
require 'faraday/http_cache/version'
|
|
9
10
|
|
|
10
11
|
module Faraday
|
|
11
12
|
# Public: The middleware responsible for caching and serving responses.
|
|
@@ -60,6 +61,9 @@ module Faraday
|
|
|
60
61
|
# The response was cached and can still be used.
|
|
61
62
|
:fresh,
|
|
62
63
|
|
|
64
|
+
# The response was stale but served while revalidating asynchronously.
|
|
65
|
+
:stale,
|
|
66
|
+
|
|
63
67
|
# The response was cached and the server has validated it with a 304 response.
|
|
64
68
|
:valid,
|
|
65
69
|
|
|
@@ -84,6 +88,8 @@ module Faraday
|
|
|
84
88
|
# :shared_cache - A flag to mark the middleware as a shared cache or not.
|
|
85
89
|
# :instrumenter - An instrumentation object that should respond to 'instrument'.
|
|
86
90
|
# :instrument_name - The String name of the instrument being reported on (optional).
|
|
91
|
+
# :on_stale - A Proc/lambda called with request:, env:, cached_response: when
|
|
92
|
+
# a stale response is served within stale-while-revalidate window.
|
|
87
93
|
# :logger - A logger object.
|
|
88
94
|
# :max_entries - The maximum number of entries to store per cache key. This option is only
|
|
89
95
|
# used when using the +ByUrl+ cache strategy.
|
|
@@ -103,7 +109,7 @@ module Faraday
|
|
|
103
109
|
# # Initialize the middleware with a MemoryStore and logger
|
|
104
110
|
# store = ActiveSupport::Cache.lookup_store
|
|
105
111
|
# Faraday::HttpCache.new(app, store: store, logger: my_logger)
|
|
106
|
-
def initialize(app, options = {})
|
|
112
|
+
def initialize(app, options = {}, &block)
|
|
107
113
|
super(app)
|
|
108
114
|
|
|
109
115
|
options = options.dup
|
|
@@ -111,6 +117,7 @@ module Faraday
|
|
|
111
117
|
@shared_cache = options.delete(:shared_cache) { true }
|
|
112
118
|
@instrumenter = options.delete(:instrumenter)
|
|
113
119
|
@instrument_name = options.delete(:instrument_name) { EVENT_NAME }
|
|
120
|
+
@on_stale = options.delete(:on_stale) || block
|
|
114
121
|
|
|
115
122
|
strategy = options.delete(:strategy) { Strategies::ByUrl }
|
|
116
123
|
|
|
@@ -189,11 +196,15 @@ module Faraday
|
|
|
189
196
|
def process(env)
|
|
190
197
|
entry = @strategy.read(@request)
|
|
191
198
|
|
|
192
|
-
return fetch(env) if entry.nil?
|
|
199
|
+
return fetch(env) if entry.nil? || !reusable?(entry)
|
|
193
200
|
|
|
194
201
|
if entry.fresh? && !@request.no_cache?
|
|
195
202
|
response = entry.to_response(env)
|
|
196
203
|
trace :fresh
|
|
204
|
+
elsif entry.stale_while_revalidate? && !@request.no_cache?
|
|
205
|
+
response = entry.to_response(env)
|
|
206
|
+
trace :stale
|
|
207
|
+
on_stale(env, entry)
|
|
197
208
|
else
|
|
198
209
|
trace :must_revalidate
|
|
199
210
|
response = validate(entry, env)
|
|
@@ -259,7 +270,7 @@ module Faraday
|
|
|
259
270
|
#
|
|
260
271
|
# Returns nothing.
|
|
261
272
|
def store(response)
|
|
262
|
-
if
|
|
273
|
+
if storable?(response)
|
|
263
274
|
trace :store
|
|
264
275
|
@strategy.write(@request, response)
|
|
265
276
|
else
|
|
@@ -267,6 +278,41 @@ module Faraday
|
|
|
267
278
|
end
|
|
268
279
|
end
|
|
269
280
|
|
|
281
|
+
# Internal: Checks if the response may be stored by this cache instance.
|
|
282
|
+
# A shared cache also refuses responses to requests that carried an
|
|
283
|
+
# 'Authorization' header unless the response explicitly allows it
|
|
284
|
+
# (RFC 9111 section 3.5), so what is never stored is never served to
|
|
285
|
+
# another caller.
|
|
286
|
+
#
|
|
287
|
+
# response - a 'Faraday::HttpCache::Response' instance.
|
|
288
|
+
#
|
|
289
|
+
# Returns true or false.
|
|
290
|
+
def storable?(response)
|
|
291
|
+
return response.cacheable_in_private_cache? unless shared_cache?
|
|
292
|
+
return false if authorization_bearing? && !response.shared_cache_authorized?
|
|
293
|
+
|
|
294
|
+
response.cacheable_in_shared_cache?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Internal: Checks if a stored entry may be served for the current request.
|
|
298
|
+
# Entries written by earlier versions of this middleware may be responses
|
|
299
|
+
# to authenticated requests that a shared cache must not reuse; such an
|
|
300
|
+
# entry is treated as a miss and replaced.
|
|
301
|
+
#
|
|
302
|
+
# entry - a 'Faraday::HttpCache::Response' read from the strategy.
|
|
303
|
+
#
|
|
304
|
+
# Returns true or false.
|
|
305
|
+
def reusable?(entry)
|
|
306
|
+
return true unless shared_cache? && authorization_bearing?
|
|
307
|
+
|
|
308
|
+
entry.shared_cache_authorized?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Internal: Checks if the current request carries an 'Authorization' header.
|
|
312
|
+
def authorization_bearing?
|
|
313
|
+
!@request.headers['Authorization'].nil?
|
|
314
|
+
end
|
|
315
|
+
|
|
270
316
|
def delete(request, response)
|
|
271
317
|
headers = %w[Location Content-Location]
|
|
272
318
|
headers.each do |header|
|
|
@@ -312,6 +358,14 @@ module Faraday
|
|
|
312
358
|
Request.from_env(env)
|
|
313
359
|
end
|
|
314
360
|
|
|
361
|
+
def on_stale(env, cached_response)
|
|
362
|
+
return unless @on_stale
|
|
363
|
+
|
|
364
|
+
@on_stale.call(request: @request, env: env, cached_response: cached_response)
|
|
365
|
+
rescue StandardError => e
|
|
366
|
+
@logger&.warn("HTTP Cache: on_stale callback failed: #{e.class}: #{e.message}")
|
|
367
|
+
end
|
|
368
|
+
|
|
315
369
|
# Internal: Logs the trace info about the incoming request
|
|
316
370
|
# and how the middleware handled it.
|
|
317
371
|
# This method does nothing if theresn't a logger present.
|
data/spec/cache_control_spec.rb
CHANGED
|
@@ -106,4 +106,14 @@ describe Faraday::HttpCache::CacheControl do
|
|
|
106
106
|
cache_control = Faraday::HttpCache::CacheControl.new('max-age=600')
|
|
107
107
|
expect(cache_control).not_to be_no_cache
|
|
108
108
|
end
|
|
109
|
+
|
|
110
|
+
it 'responds to #stale_while_revalidate with an integer when directive present' do
|
|
111
|
+
cache_control = Faraday::HttpCache::CacheControl.new('public, max-age=60, stale-while-revalidate=300')
|
|
112
|
+
expect(cache_control.stale_while_revalidate).to eq(300)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'responds to #stale_while_revalidate with nil when directive absent' do
|
|
116
|
+
cache_control = Faraday::HttpCache::CacheControl.new('public, max-age=60')
|
|
117
|
+
expect(cache_control.stale_while_revalidate).to be_nil
|
|
118
|
+
end
|
|
109
119
|
end
|
data/spec/http_cache_spec.rb
CHANGED
|
@@ -121,6 +121,48 @@ describe Faraday::HttpCache do
|
|
|
121
121
|
expect(logger).to receive(:debug) { |&block| expect(block.call).to eq('HTTP Cache: [GET /private] miss, uncacheable') }
|
|
122
122
|
client.get('private')
|
|
123
123
|
end
|
|
124
|
+
|
|
125
|
+
describe 'responses to requests with an "Authorization" header' do
|
|
126
|
+
def get_as(user, path = 'authenticated')
|
|
127
|
+
client.get(path) { |request| request.headers['Authorization'] = "Bearer #{user}" }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'does not serve one caller the response cached for another' do
|
|
131
|
+
alice = get_as('alice')
|
|
132
|
+
bob = get_as('bob')
|
|
133
|
+
|
|
134
|
+
expect(alice.body).to eq('1:Bearer alice')
|
|
135
|
+
expect(bob.body).to eq('2:Bearer bob')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'logs that the response is uncacheable' do
|
|
139
|
+
expect(logger).to receive(:debug) { |&block| expect(block.call).to eq('HTTP Cache: [GET /authenticated] miss, uncacheable') }
|
|
140
|
+
get_as('alice')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'caches responses that are explicitly marked as public' do
|
|
144
|
+
get_as('alice', 'authenticated-public')
|
|
145
|
+
bob = get_as('bob', 'authenticated-public')
|
|
146
|
+
|
|
147
|
+
expect(bob.body).to eq('1:Bearer alice')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'does not serve entries stored before the authorization check existed' do
|
|
151
|
+
store = Faraday::HttpCache::MemoryStore.new
|
|
152
|
+
clients = [false, true].map do |shared|
|
|
153
|
+
Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
|
|
154
|
+
stack.use Faraday::HttpCache, store: store, shared_cache: shared
|
|
155
|
+
stack.adapter ENV['FARADAY_ADAPTER'].to_sym
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
private_client, shared_client = clients
|
|
159
|
+
|
|
160
|
+
private_client.get('authenticated') { |request| request.headers['Authorization'] = 'Bearer alice' }
|
|
161
|
+
bob = shared_client.get('authenticated') { |request| request.headers['Authorization'] = 'Bearer bob' }
|
|
162
|
+
|
|
163
|
+
expect(bob.body).to eq('2:Bearer bob')
|
|
164
|
+
end
|
|
165
|
+
end
|
|
124
166
|
end
|
|
125
167
|
|
|
126
168
|
describe 'when acting as a private cache' do
|
|
@@ -135,6 +177,13 @@ describe Faraday::HttpCache do
|
|
|
135
177
|
expect(logger).to receive(:debug) { |&block| expect(block.call).to eq('HTTP Cache: [GET /private] miss, store') }
|
|
136
178
|
client.get('private')
|
|
137
179
|
end
|
|
180
|
+
|
|
181
|
+
it 'caches responses to requests with an "Authorization" header' do
|
|
182
|
+
client.get('authenticated') { |request| request.headers['Authorization'] = 'Bearer alice' }
|
|
183
|
+
bob = client.get('authenticated') { |request| request.headers['Authorization'] = 'Bearer bob' }
|
|
184
|
+
|
|
185
|
+
expect(bob.body).to eq('1:Bearer alice')
|
|
186
|
+
end
|
|
138
187
|
end
|
|
139
188
|
|
|
140
189
|
it 'does not cache responses with a explicit no-store directive' do
|
|
@@ -221,6 +270,61 @@ describe Faraday::HttpCache do
|
|
|
221
270
|
client.get('get')
|
|
222
271
|
end
|
|
223
272
|
|
|
273
|
+
describe 'stale-while-revalidate' do
|
|
274
|
+
let(:on_stale) { double('stale callback', call: nil) }
|
|
275
|
+
let(:options) { { logger: logger, on_stale: on_stale } }
|
|
276
|
+
|
|
277
|
+
it 'serves stale cached responses within stale-while-revalidate window' do
|
|
278
|
+
expect(client.get('stale-while-revalidate').body).to eq('1')
|
|
279
|
+
|
|
280
|
+
response = client.get('stale-while-revalidate')
|
|
281
|
+
expect(response.body).to eq('1')
|
|
282
|
+
expect(response.env[:http_cache_trace]).to eq([:stale])
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'invokes the on_stale callback with request, env and cached response' do
|
|
286
|
+
client.get('stale-while-revalidate')
|
|
287
|
+
|
|
288
|
+
expect(on_stale).to receive(:call).with(
|
|
289
|
+
request: an_instance_of(Faraday::HttpCache::Request),
|
|
290
|
+
env: an_instance_of(Faraday::Env),
|
|
291
|
+
cached_response: an_instance_of(Faraday::HttpCache::Response)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
client.get('stale-while-revalidate')
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'ignores on_stale callback errors and still serves stale response' do
|
|
298
|
+
failing_callback = lambda do |request:, env:, cached_response:|
|
|
299
|
+
request && env && cached_response
|
|
300
|
+
raise 'boom'
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
local_client = Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
|
|
304
|
+
stack.use Faraday::HttpCache, logger: logger, on_stale: failing_callback
|
|
305
|
+
adapter = ENV['FARADAY_ADAPTER']
|
|
306
|
+
stack.headers['X-Faraday-Adapter'] = adapter
|
|
307
|
+
stack.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
308
|
+
stack.adapter adapter.to_sym
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
local_client.get('stale-while-revalidate')
|
|
312
|
+
expect(logger).to receive(:warn).with(/on_stale callback failed: RuntimeError: boom/)
|
|
313
|
+
|
|
314
|
+
response = local_client.get('stale-while-revalidate')
|
|
315
|
+
expect(response.body).to eq('1')
|
|
316
|
+
expect(response.env[:http_cache_trace]).to eq([:stale])
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'revalidates when stale-while-revalidate window has expired' do
|
|
320
|
+
expect(client.get('stale-while-revalidate-expired').body).to eq('1')
|
|
321
|
+
|
|
322
|
+
response = client.get('stale-while-revalidate-expired')
|
|
323
|
+
expect(response.body).to eq('1')
|
|
324
|
+
expect(response.env[:http_cache_trace]).to eq(%i[must_revalidate valid store])
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
224
328
|
it 'sends the "Last-Modified" header on response validation' do
|
|
225
329
|
client.get('timestamped')
|
|
226
330
|
expect(client.get('timestamped').body).to eq('1')
|
|
@@ -43,6 +43,16 @@ describe 'Instrumentation' do
|
|
|
43
43
|
expect(events.last.payload.fetch(:cache_status)).to eq(:fresh)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
it 'is :stale if the cache entry is stale but can be served while revalidating' do
|
|
47
|
+
backend.get('/hello') do
|
|
48
|
+
[200, { 'Cache-Control' => 'public, max-age=0, stale-while-revalidate=60', 'Date' => Time.now.httpdate, 'Etag' => '123ABCD' }, '']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
client.get('/hello') # miss
|
|
52
|
+
client.get('/hello') # stale
|
|
53
|
+
expect(events.last.payload.fetch(:cache_status)).to eq(:stale)
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
it 'is :valid if the cache entry can be validated against the upstream' do
|
|
47
57
|
backend.get('/hello') do
|
|
48
58
|
headers = {
|
data/spec/response_spec.rb
CHANGED
|
@@ -199,6 +199,29 @@ describe Faraday::HttpCache::Response do
|
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
describe 'stale while revalidate' do
|
|
203
|
+
it 'is true when response is stale but inside stale-while-revalidate window' do
|
|
204
|
+
headers = { 'Cache-Control' => 'max-age=60, stale-while-revalidate=20', 'Date' => (Time.now - 70).httpdate }
|
|
205
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
|
206
|
+
|
|
207
|
+
expect(response).to be_stale_while_revalidate
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'is false when response is stale and outside stale-while-revalidate window' do
|
|
211
|
+
headers = { 'Cache-Control' => 'max-age=60, stale-while-revalidate=20', 'Date' => (Time.now - 90).httpdate }
|
|
212
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
|
213
|
+
|
|
214
|
+
expect(response).not_to be_stale_while_revalidate
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'is false when no-cache is set' do
|
|
218
|
+
headers = { 'Cache-Control' => 'max-age=60, stale-while-revalidate=20, no-cache', 'Date' => (Time.now - 70).httpdate }
|
|
219
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
|
220
|
+
|
|
221
|
+
expect(response).not_to be_stale_while_revalidate
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
202
225
|
describe 'response unboxing' do
|
|
203
226
|
subject { described_class.new(status: 200, response_headers: {}, body: 'Hi!', reason_phrase: 'Success') }
|
|
204
227
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -16,6 +16,20 @@ describe Faraday::HttpCache::Strategies::ByUrl do
|
|
|
16
16
|
let(:strategy) { described_class.new(store: cache) }
|
|
17
17
|
subject { strategy }
|
|
18
18
|
|
|
19
|
+
describe 'deserializing entries' do
|
|
20
|
+
let(:response) { double(serializable_hash: { response_headers: { 'json_class' => 'JsonGadget' } }) }
|
|
21
|
+
|
|
22
|
+
before { JsonGadget.invocations.clear }
|
|
23
|
+
|
|
24
|
+
it 'never instantiates classes named by the cached data' do
|
|
25
|
+
strategy.write(request, response)
|
|
26
|
+
cached = strategy.read(request)
|
|
27
|
+
|
|
28
|
+
expect(JsonGadget.invocations).to be_empty
|
|
29
|
+
expect(cached.payload[:response_headers]['json_class']).to eq('JsonGadget')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
19
33
|
describe 'Cache configuration' do
|
|
20
34
|
it 'uses a MemoryStore by default' do
|
|
21
35
|
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
|
@@ -23,6 +23,20 @@ describe Faraday::HttpCache::Strategies::ByVary do
|
|
|
23
23
|
let(:strategy) { described_class.new(store: cache) }
|
|
24
24
|
subject { strategy }
|
|
25
25
|
|
|
26
|
+
describe 'deserializing entries' do
|
|
27
|
+
let(:response_payload) { { response_headers: { 'Vary' => vary, 'json_class' => 'JsonGadget' } } }
|
|
28
|
+
|
|
29
|
+
before { JsonGadget.invocations.clear }
|
|
30
|
+
|
|
31
|
+
it 'never instantiates classes named by the cached data' do
|
|
32
|
+
strategy.write(request, response)
|
|
33
|
+
cached = strategy.read(request)
|
|
34
|
+
|
|
35
|
+
expect(JsonGadget.invocations).to be_empty
|
|
36
|
+
expect(cached.payload[:response_headers]['json_class']).to eq('JsonGadget')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
describe 'storing responses' do
|
|
27
41
|
shared_examples 'A strategy with serialization' do
|
|
28
42
|
it 'writes the response object to the underlying cache' do
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A class that records every attempt to build it through JSON.load's
|
|
4
|
+
# create_additions hook, so specs can assert cached entries never do that.
|
|
5
|
+
class JsonGadget
|
|
6
|
+
def self.invocations
|
|
7
|
+
@invocations ||= []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.json_create(attributes)
|
|
11
|
+
invocations << attributes
|
|
12
|
+
new
|
|
13
|
+
end
|
|
14
|
+
end
|
data/spec/support/test_app.rb
CHANGED
|
@@ -61,6 +61,18 @@ class TestApp < Sinatra::Base
|
|
|
61
61
|
[200, { 'Cache-Control' => 'max-age=200' }, increment_counter]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
get '/stale-while-revalidate' do
|
|
65
|
+
[200, { 'Cache-Control' => 'max-age=0, stale-while-revalidate=120', 'Date' => Time.now.httpdate, 'ETag' => 'stale' }, increment_counter]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
get '/stale-while-revalidate-expired' do
|
|
69
|
+
if env['HTTP_IF_NONE_MATCH'] == '1'
|
|
70
|
+
[304, {}, '']
|
|
71
|
+
else
|
|
72
|
+
[200, { 'Cache-Control' => 'max-age=0, stale-while-revalidate=1', 'Date' => settings.yesterday, 'ETag' => '1' }, increment_counter]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
post '/delete-with-location' do
|
|
65
77
|
[200, { 'Location' => "#{request.base_url}/get" }, '']
|
|
66
78
|
end
|
|
@@ -73,6 +85,14 @@ class TestApp < Sinatra::Base
|
|
|
73
85
|
halt 405
|
|
74
86
|
end
|
|
75
87
|
|
|
88
|
+
get '/authenticated' do
|
|
89
|
+
[200, { 'Cache-Control' => 'max-age=200' }, "#{increment_counter}:#{env['HTTP_AUTHORIZATION']}"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
get '/authenticated-public' do
|
|
93
|
+
[200, { 'Cache-Control' => 'public, max-age=200' }, "#{increment_counter}:#{env['HTTP_AUTHORIZATION']}"]
|
|
94
|
+
end
|
|
95
|
+
|
|
76
96
|
get '/private' do
|
|
77
97
|
[200, { 'Cache-Control' => 'private, max-age=100' }, increment_counter]
|
|
78
98
|
end
|
metadata
CHANGED
|
@@ -1,16 +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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Mazza
|
|
8
8
|
- George Guimarães
|
|
9
9
|
- Gustavo Araujo
|
|
10
|
-
autorequire:
|
|
10
|
+
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-
|
|
13
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: faraday
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/faraday/http_cache/strategies/base_strategy.rb
|
|
47
47
|
- lib/faraday/http_cache/strategies/by_url.rb
|
|
48
48
|
- lib/faraday/http_cache/strategies/by_vary.rb
|
|
49
|
+
- lib/faraday/http_cache/version.rb
|
|
49
50
|
- spec/binary_spec.rb
|
|
50
51
|
- spec/cache_control_spec.rb
|
|
51
52
|
- spec/http_cache_spec.rb
|
|
@@ -59,6 +60,7 @@ files:
|
|
|
59
60
|
- spec/strategies/by_url_spec.rb
|
|
60
61
|
- spec/strategies/by_vary_spec.rb
|
|
61
62
|
- spec/support/empty.png
|
|
63
|
+
- spec/support/json_gadget.rb
|
|
62
64
|
- spec/support/test_app.rb
|
|
63
65
|
- spec/support/test_server.rb
|
|
64
66
|
- spec/validation_spec.rb
|
|
@@ -66,7 +68,7 @@ homepage: https://github.com/sourcelevel/faraday-http-cache
|
|
|
66
68
|
licenses:
|
|
67
69
|
- Apache-2.0
|
|
68
70
|
metadata: {}
|
|
69
|
-
post_install_message:
|
|
71
|
+
post_install_message:
|
|
70
72
|
rdoc_options: []
|
|
71
73
|
require_paths:
|
|
72
74
|
- lib
|
|
@@ -81,24 +83,25 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
83
|
- !ruby/object:Gem::Version
|
|
82
84
|
version: '0'
|
|
83
85
|
requirements: []
|
|
84
|
-
rubygems_version: 3.
|
|
85
|
-
signing_key:
|
|
86
|
+
rubygems_version: 3.5.22
|
|
87
|
+
signing_key:
|
|
86
88
|
specification_version: 4
|
|
87
89
|
summary: A Faraday middleware that stores and validates cache expiration.
|
|
88
90
|
test_files:
|
|
89
|
-
- spec/
|
|
90
|
-
- spec/
|
|
91
|
-
- spec/
|
|
92
|
-
- spec/strategies/by_url_spec.rb
|
|
93
|
-
- spec/strategies/base_strategy_spec.rb
|
|
94
|
-
- spec/json_spec.rb
|
|
91
|
+
- spec/binary_spec.rb
|
|
92
|
+
- spec/cache_control_spec.rb
|
|
93
|
+
- spec/http_cache_spec.rb
|
|
95
94
|
- spec/instrumentation_spec.rb
|
|
95
|
+
- spec/json_spec.rb
|
|
96
|
+
- spec/request_spec.rb
|
|
97
|
+
- spec/response_spec.rb
|
|
98
|
+
- spec/spec_helper.rb
|
|
96
99
|
- spec/storage_spec.rb
|
|
97
|
-
- spec/
|
|
98
|
-
- spec/
|
|
99
|
-
- spec/
|
|
100
|
+
- spec/strategies/base_strategy_spec.rb
|
|
101
|
+
- spec/strategies/by_url_spec.rb
|
|
102
|
+
- spec/strategies/by_vary_spec.rb
|
|
100
103
|
- spec/support/empty.png
|
|
104
|
+
- spec/support/json_gadget.rb
|
|
105
|
+
- spec/support/test_app.rb
|
|
101
106
|
- spec/support/test_server.rb
|
|
102
|
-
- spec/
|
|
103
|
-
- spec/cache_control_spec.rb
|
|
104
|
-
- spec/response_spec.rb
|
|
107
|
+
- spec/validation_spec.rb
|