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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d83bc178f6be84f8807a6357f4d60aefbdde828a0b63355b4517aa7a2e3dca0
4
- data.tar.gz: d1543b1549fc3924d15bba59947f7b46aa05609043328f7ce84bec56cea16e45
3
+ metadata.gz: 00c9816f80d3f4a4f700bf153c6e4b20eed4f28fc0fcbc59d3e6a9135dd864fb
4
+ data.tar.gz: 1298dc1a38f6529db56b8d862524268908ac09a2825678450958955b2af8f096
5
5
  SHA512:
6
- metadata.gz: 863ec060abd499c032a62be09cbb4ac5c0d00529a3af5c2ffff79c2c538eb56adbb4b2932eb3be55670f00777fe887383398b64cb457510a8a822176367e9040
7
- data.tar.gz: 5817a00b39242a99cd7539b2bf883606faa68bad7aba448e7dda23490de02ee9de3814b6f7b3932edf1d86e7d66cd011badc8a637fa7725ca0e89cb018b6a9aa
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 the stdlib
66
- [Marshal][marshal] instead. Alternatively you could use another json library like `oj` or `yajl-ruby`.
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` and `s-maxage` directives are checked.
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 2616. This means it does not cache
189
- responses with `Cache-Control: private`. This behavior can be changed by passing in the
190
- `:shared_cache` configuration option:
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
- @serializer.load(object).transform_keys(&:to_sym)
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)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ class HttpCache
5
+ VERSION = '2.8.0'
6
+ end
7
+ end
@@ -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 shared_cache? ? response.cacheable_in_shared_cache? : response.cacheable_in_private_cache?
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.
@@ -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
@@ -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 = {
@@ -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,7 @@ require 'active_support/cache'
16
16
 
17
17
  require 'support/test_app'
18
18
  require 'support/test_server'
19
+ require 'support/json_gadget'
19
20
 
20
21
  server = TestServer.new
21
22
 
@@ -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
@@ -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.6.1
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-01-19 00:00:00.000000000 Z
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.0.3.1
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/spec_helper.rb
90
- - spec/validation_spec.rb
91
- - spec/strategies/by_vary_spec.rb
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/http_cache_spec.rb
98
- - spec/binary_spec.rb
99
- - spec/support/test_app.rb
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/request_spec.rb
103
- - spec/cache_control_spec.rb
104
- - spec/response_spec.rb
107
+ - spec/validation_spec.rb