faraday-http-cache 2.7.0 → 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: 9178a3cc4f43cad31ef2a21a3cac0ef2ccf1a3ea86e5667df90aee9aa58da4d3
4
- data.tar.gz: 28ea9a28a3fee4f78e8da8cf7af3d84f051836475ef8f6aca0e1533f0b0bafa5
3
+ metadata.gz: 00c9816f80d3f4a4f700bf153c6e4b20eed4f28fc0fcbc59d3e6a9135dd864fb
4
+ data.tar.gz: 1298dc1a38f6529db56b8d862524268908ac09a2825678450958955b2af8f096
5
5
  SHA512:
6
- metadata.gz: 1f7d626be02155cea5f5a4ad8c26070da0a35b3e7e7c0d0af5d4441da3ccd75d46c1e697700510a0abb740e0e0ce3185ca26688f946c907d2fa0a4aab3dec3e0
7
- data.tar.gz: f2f881a67a1a7684ef8029932ffb51673c1ce23b49e09178fab248e6bb2648969d99bf1725d5878e9d33b682050577f441520297970cac22db0cc88ceb6e914f
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|
@@ -214,9 +216,11 @@ The `max-age`, `must-revalidate`, `proxy-revalidate`, `s-maxage` and
214
216
 
215
217
  ### Shared vs. non-shared caches
216
218
 
217
- By default, the middleware acts as a "shared cache" per RFC 2616. This means it does not cache
218
- responses with `Cache-Control: private`. This behavior can be changed by passing in the
219
- `: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:
220
224
 
221
225
  ```ruby
222
226
  client = Faraday.new do |builder|
@@ -98,6 +98,22 @@ module Faraday
98
98
  cacheable?(false)
99
99
  end
100
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
+
101
117
  # Internal: Gets the response age in seconds.
102
118
  #
103
119
  # Returns the 'Age' header if present, or subtracts the response 'date'
@@ -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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Faraday
4
4
  class HttpCache
5
- VERSION = '2.7.0'
5
+ VERSION = '2.8.0'
6
6
  end
7
7
  end
@@ -196,7 +196,7 @@ module Faraday
196
196
  def process(env)
197
197
  entry = @strategy.read(@request)
198
198
 
199
- return fetch(env) if entry.nil?
199
+ return fetch(env) if entry.nil? || !reusable?(entry)
200
200
 
201
201
  if entry.fresh? && !@request.no_cache?
202
202
  response = entry.to_response(env)
@@ -270,7 +270,7 @@ module Faraday
270
270
  #
271
271
  # Returns nothing.
272
272
  def store(response)
273
- if shared_cache? ? response.cacheable_in_shared_cache? : response.cacheable_in_private_cache?
273
+ if storable?(response)
274
274
  trace :store
275
275
  @strategy.write(@request, response)
276
276
  else
@@ -278,6 +278,41 @@ module Faraday
278
278
  end
279
279
  end
280
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
+
281
316
  def delete(request, response)
282
317
  headers = %w[Location Content-Location]
283
318
  headers.each do |header|
@@ -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
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
@@ -85,6 +85,14 @@ class TestApp < Sinatra::Base
85
85
  halt 405
86
86
  end
87
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
+
88
96
  get '/private' do
89
97
  [200, { 'Cache-Control' => 'private, max-age=100' }, increment_counter]
90
98
  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.7.0
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
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 1980-01-02 00:00:00.000000000 Z
13
+ date: 2026-09-15 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: faraday
@@ -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,6 +68,7 @@ homepage: https://github.com/sourcelevel/faraday-http-cache
66
68
  licenses:
67
69
  - Apache-2.0
68
70
  metadata: {}
71
+ post_install_message:
69
72
  rdoc_options: []
70
73
  require_paths:
71
74
  - lib
@@ -80,7 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
83
  - !ruby/object:Gem::Version
81
84
  version: '0'
82
85
  requirements: []
83
- rubygems_version: 4.0.3
86
+ rubygems_version: 3.5.22
87
+ signing_key:
84
88
  specification_version: 4
85
89
  summary: A Faraday middleware that stores and validates cache expiration.
86
90
  test_files:
@@ -97,6 +101,7 @@ test_files:
97
101
  - spec/strategies/by_url_spec.rb
98
102
  - spec/strategies/by_vary_spec.rb
99
103
  - spec/support/empty.png
104
+ - spec/support/json_gadget.rb
100
105
  - spec/support/test_app.rb
101
106
  - spec/support/test_server.rb
102
107
  - spec/validation_spec.rb