faraday-http-cache 2.6.1 → 2.7.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: 9178a3cc4f43cad31ef2a21a3cac0ef2ccf1a3ea86e5667df90aee9aa58da4d3
4
+ data.tar.gz: 28ea9a28a3fee4f78e8da8cf7af3d84f051836475ef8f6aca0e1533f0b0bafa5
5
5
  SHA512:
6
- metadata.gz: 863ec060abd499c032a62be09cbb4ac5c0d00529a3af5c2ffff79c2c538eb56adbb4b2932eb3be55670f00777fe887383398b64cb457510a8a822176367e9040
7
- data.tar.gz: 5817a00b39242a99cd7539b2bf883606faa68bad7aba448e7dda23490de02ee9de3814b6f7b3932edf1d86e7d66cd011badc8a637fa7725ca0e89cb018b6a9aa
6
+ metadata.gz: 1f7d626be02155cea5f5a4ad8c26070da0a35b3e7e7c0d0af5d4441da3ccd75d46c1e697700510a0abb740e0e0ce3185ca26688f946c907d2fa0a4aab3dec3e0
7
+ data.tar.gz: f2f881a67a1a7684ef8029932ffb51673c1ce23b49e09178fab248e6bb2648969d99bf1725d5878e9d33b682050577f441520297970cac22db0cc88ceb6e914f
data/README.md CHANGED
@@ -72,6 +72,31 @@ client = Faraday.new do |builder|
72
72
  end
73
73
  ```
74
74
 
75
+ ### Stale-While-Revalidate and background refresh hooks
76
+
77
+ The middleware supports `stale-while-revalidate` directives from the `Cache-Control` header.
78
+ When a cached response is stale but still inside the `stale-while-revalidate` window, the middleware
79
+ will serve the stale response immediately.
80
+
81
+ You can provide an `:on_stale` callback to trigger your own asynchronous refresh logic:
82
+
83
+ ```ruby
84
+ client = Faraday.new do |builder|
85
+ builder.use :http_cache,
86
+ store: Rails.cache,
87
+ on_stale: lambda { |request:, env:, cached_response:|
88
+ RefreshApiCacheJob.perform_later(request.url.to_s)
89
+ }
90
+ builder.adapter Faraday.default_adapter
91
+ end
92
+ ```
93
+
94
+ The callback receives:
95
+
96
+ - `request`: `Faraday::HttpCache::Request`
97
+ - `env`: current `Faraday::Env`
98
+ - `cached_response`: `Faraday::HttpCache::Response`
99
+
75
100
  ### Strategies
76
101
 
77
102
  You can provide a `:strategy` option to the middleware to specify the strategy to use.
@@ -140,6 +165,8 @@ processes a request. In the event payload, `:env` contains the response Faraday
140
165
  - `:valid` means that the cached response *could* be validated against the server.
141
166
  - `:fresh` means that the cached response was still fresh and could be returned without even
142
167
  calling the server.
168
+ - `:stale` means that the cached response was stale, but served while inside
169
+ `stale-while-revalidate` window.
143
170
 
144
171
  ```ruby
145
172
  client = Faraday.new do |builder|
@@ -154,7 +181,7 @@ ActiveSupport::Notifications.subscribe "http_cache.faraday" do |*args|
154
181
  statsd = Statsd.new
155
182
 
156
183
  case cache_status
157
- when :fresh, :valid
184
+ when :fresh, :valid, :stale
158
185
  statsd.increment('api-calls.cache_hits')
159
186
  when :invalid, :miss
160
187
  statsd.increment('api-calls.cache_misses')
@@ -168,6 +195,7 @@ end
168
195
 
169
196
  You can clone this repository, install its dependencies with Bundler (run `bundle install`) and
170
197
  execute the files under the `examples` directory to see a sample of the middleware usage.
198
+ For stale-while-revalidate behavior with `:on_stale`, see `examples/stale_while_revalidate.rb`.
171
199
 
172
200
  ## What gets cached?
173
201
 
@@ -181,7 +209,8 @@ The middleware will use the following headers to make caching decisions:
181
209
 
182
210
  ### Cache-Control
183
211
 
184
- The `max-age`, `must-revalidate`, `proxy-revalidate` and `s-maxage` directives are checked.
212
+ The `max-age`, `must-revalidate`, `proxy-revalidate`, `s-maxage` and
213
+ `stale-while-revalidate` directives are checked.
185
214
 
186
215
  ### Shared vs. non-shared caches
187
216
 
@@ -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.
@@ -123,6 +135,13 @@ module Faraday
123
135
  (expires && (expires - @now))
124
136
  end
125
137
 
138
+ # Internal: Gets the stale-while-revalidate value in seconds.
139
+ #
140
+ # Returns an Integer or nil.
141
+ def stale_while_revalidate
142
+ cache_control.stale_while_revalidate
143
+ end
144
+
126
145
  # Internal: Creates a new 'Faraday::Response', merging the stored
127
146
  # response with the supplied 'env' object.
128
147
  #
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ class HttpCache
5
+ VERSION = '2.7.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
 
@@ -194,6 +201,10 @@ module Faraday
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)
@@ -312,6 +323,14 @@ module Faraday
312
323
  Request.from_env(env)
313
324
  end
314
325
 
326
+ def on_stale(env, cached_response)
327
+ return unless @on_stale
328
+
329
+ @on_stale.call(request: @request, env: env, cached_response: cached_response)
330
+ rescue StandardError => e
331
+ @logger&.warn("HTTP Cache: on_stale callback failed: #{e.class}: #{e.message}")
332
+ end
333
+
315
334
  # Internal: Logs the trace info about the incoming request
316
335
  # and how the middleware handled it.
317
336
  # 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
@@ -221,6 +221,61 @@ describe Faraday::HttpCache do
221
221
  client.get('get')
222
222
  end
223
223
 
224
+ describe 'stale-while-revalidate' do
225
+ let(:on_stale) { double('stale callback', call: nil) }
226
+ let(:options) { { logger: logger, on_stale: on_stale } }
227
+
228
+ it 'serves stale cached responses within stale-while-revalidate window' do
229
+ expect(client.get('stale-while-revalidate').body).to eq('1')
230
+
231
+ response = client.get('stale-while-revalidate')
232
+ expect(response.body).to eq('1')
233
+ expect(response.env[:http_cache_trace]).to eq([:stale])
234
+ end
235
+
236
+ it 'invokes the on_stale callback with request, env and cached response' do
237
+ client.get('stale-while-revalidate')
238
+
239
+ expect(on_stale).to receive(:call).with(
240
+ request: an_instance_of(Faraday::HttpCache::Request),
241
+ env: an_instance_of(Faraday::Env),
242
+ cached_response: an_instance_of(Faraday::HttpCache::Response)
243
+ )
244
+
245
+ client.get('stale-while-revalidate')
246
+ end
247
+
248
+ it 'ignores on_stale callback errors and still serves stale response' do
249
+ failing_callback = lambda do |request:, env:, cached_response:|
250
+ request && env && cached_response
251
+ raise 'boom'
252
+ end
253
+
254
+ local_client = Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
255
+ stack.use Faraday::HttpCache, logger: logger, on_stale: failing_callback
256
+ adapter = ENV['FARADAY_ADAPTER']
257
+ stack.headers['X-Faraday-Adapter'] = adapter
258
+ stack.headers['Content-Type'] = 'application/x-www-form-urlencoded'
259
+ stack.adapter adapter.to_sym
260
+ end
261
+
262
+ local_client.get('stale-while-revalidate')
263
+ expect(logger).to receive(:warn).with(/on_stale callback failed: RuntimeError: boom/)
264
+
265
+ response = local_client.get('stale-while-revalidate')
266
+ expect(response.body).to eq('1')
267
+ expect(response.env[:http_cache_trace]).to eq([:stale])
268
+ end
269
+
270
+ it 'revalidates when stale-while-revalidate window has expired' do
271
+ expect(client.get('stale-while-revalidate-expired').body).to eq('1')
272
+
273
+ response = client.get('stale-while-revalidate-expired')
274
+ expect(response.body).to eq('1')
275
+ expect(response.env[:http_cache_trace]).to eq(%i[must_revalidate valid store])
276
+ end
277
+ end
278
+
224
279
  it 'sends the "Last-Modified" header on response validation' do
225
280
  client.get('timestamped')
226
281
  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
 
@@ -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
metadata CHANGED
@@ -1,16 +1,15 @@
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.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Mazza
8
8
  - George Guimarães
9
9
  - Gustavo Araujo
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2026-01-19 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: faraday
@@ -46,6 +45,7 @@ files:
46
45
  - lib/faraday/http_cache/strategies/base_strategy.rb
47
46
  - lib/faraday/http_cache/strategies/by_url.rb
48
47
  - lib/faraday/http_cache/strategies/by_vary.rb
48
+ - lib/faraday/http_cache/version.rb
49
49
  - spec/binary_spec.rb
50
50
  - spec/cache_control_spec.rb
51
51
  - spec/http_cache_spec.rb
@@ -66,7 +66,6 @@ homepage: https://github.com/sourcelevel/faraday-http-cache
66
66
  licenses:
67
67
  - Apache-2.0
68
68
  metadata: {}
69
- post_install_message:
70
69
  rdoc_options: []
71
70
  require_paths:
72
71
  - lib
@@ -81,24 +80,23 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
83
82
  requirements: []
84
- rubygems_version: 3.0.3.1
85
- signing_key:
83
+ rubygems_version: 4.0.3
86
84
  specification_version: 4
87
85
  summary: A Faraday middleware that stores and validates cache expiration.
88
86
  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
87
+ - spec/binary_spec.rb
88
+ - spec/cache_control_spec.rb
89
+ - spec/http_cache_spec.rb
95
90
  - spec/instrumentation_spec.rb
91
+ - spec/json_spec.rb
92
+ - spec/request_spec.rb
93
+ - spec/response_spec.rb
94
+ - spec/spec_helper.rb
96
95
  - spec/storage_spec.rb
97
- - spec/http_cache_spec.rb
98
- - spec/binary_spec.rb
99
- - spec/support/test_app.rb
96
+ - spec/strategies/base_strategy_spec.rb
97
+ - spec/strategies/by_url_spec.rb
98
+ - spec/strategies/by_vary_spec.rb
100
99
  - spec/support/empty.png
100
+ - spec/support/test_app.rb
101
101
  - spec/support/test_server.rb
102
- - spec/request_spec.rb
103
- - spec/cache_control_spec.rb
104
- - spec/response_spec.rb
102
+ - spec/validation_spec.rb