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 +4 -4
- data/README.md +9 -5
- data/lib/faraday/http_cache/response.rb +16 -0
- data/lib/faraday/http_cache/strategies/base_strategy.rb +9 -2
- data/lib/faraday/http_cache/version.rb +1 -1
- data/lib/faraday/http_cache.rb +37 -2
- data/spec/http_cache_spec.rb +49 -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 +8 -0
- metadata +8 -3
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|
|
|
@@ -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
|
|
218
|
-
responses with `Cache-Control: private
|
|
219
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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|
|
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
|
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
|
@@ -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.
|
|
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:
|
|
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:
|
|
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
|