faraday-http-cache 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 962b23db73b9799330e7229f9a6d6e252705a55f3c3ad316f810132160a30522
4
- data.tar.gz: 51a43a544f924e75c00ffffd777368cf995000a3855e1ea7b9b86fb54b5f6b2a
3
+ metadata.gz: 6603be3bbf6a2840ba11f947f7e42f030ab52600912e9a62d16aaf27ee5e6446
4
+ data.tar.gz: 95b8b2f4b08525406027d47e14341f5f301409a2518332b243dc08d2bde4e693
5
5
  SHA512:
6
- metadata.gz: 57de8b27682d5aa8367fd7612ec3814c300a0856ba7e1ee61354646117a2370f216981b60f5224662236444a44669a70840fb07118602a82b66a0c886ae1540d
7
- data.tar.gz: d8e1644ed674c3c7a6c8f44adfe83eda6f9268590235330eaf0ed62ec41795a97a35f28f52bddf7c3103a8cef6c849330167b1b708a6c8124da642e371bf434b
6
+ metadata.gz: 4675c865c18641322c71b77881a74d3ca4edbf4fb1d628ac0fe7a6388d6a3c973946f1d7fa7b09d654f1eff61484a8c2d8524c966e4377c1e12854bf8114e13a
7
+ data.tar.gz: 832d7579eb75d3631e539ca7af996b9d062dbe43582468f98c349b3dbc963dde17cecf57a0202af1916297013143becba1a0fc1f32ddf92ab98490876e3efc84
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Faraday Http Cache
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/sourcelevel/faraday-http-cache.svg?branch=master)](https://travis-ci.org/sourcelevel/faraday-http-cache)
3
+ [![Gem Version](https://badge.fury.io/rb/faraday-http-cache.svg)](https://rubygems.org/gems/faraday-http-cache)
4
+ [![Build](https://github.com/sourcelevel/faraday-http-cache/actions/workflows/main.yml/badge.svg)](https://github.com/sourcelevel/faraday-http-cache/actions)
4
5
 
5
- a [Faraday](https://github.com/lostisland/faraday) middleware that respects HTTP cache,
6
+ A [Faraday](https://github.com/lostisland/faraday) middleware that respects HTTP cache,
6
7
  by checking expiration and validation of the stored responses.
7
8
 
8
9
  ## Installation
@@ -53,15 +54,15 @@ This type of store **might not be persisted across multiple processes or connect
53
54
  so it is probably not suitable for most production environments.
54
55
  Make sure that you configure a store that is suitable for you.
55
56
 
56
- The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
57
- characters in responses. For example, if your JSON returns `"name": "Raül"` then you might see
58
- errors like:
57
+ The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
58
+ characters in responses in Ruby < 3.1. For example, if your JSON returns `"name": "Raül"` then
59
+ you might see errors like:
59
60
 
60
61
  ```
61
62
  Response could not be serialized: "\xC3" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.
62
63
  ```
63
64
 
64
- For full unicode support, or if you expect to be dealing with images, you can use
65
+ For full unicode support, or if you expect to be dealing with images, you can use the stdlib
65
66
  [Marshal][marshal] instead. Alternatively you could use another json library like `oj` or `yajl-ruby`.
66
67
 
67
68
  ```ruby
@@ -71,9 +72,47 @@ client = Faraday.new do |builder|
71
72
  end
72
73
  ```
73
74
 
75
+ ### Strategies
76
+
77
+ You can provide a `:strategy` option to the middleware to specify the strategy to use.
78
+
79
+ ```ruby
80
+ client = Faraday.new do |builder|
81
+ builder.use :http_cache, store: Rails.cache, strategy: Faraday::HttpCache::Strategies::ByVary
82
+ builder.adapter Faraday.default_adapter
83
+ end
84
+ ```
85
+
86
+ Available strategies are:
87
+
88
+ #### `Faraday::HttpCache::Strategies::ByUrl`
89
+
90
+ The default strategy.
91
+ It Uses URL + HTTP method to generate cache keys and stores an array of request + response for each key.
92
+
93
+ #### `Faraday::HttpCache::Strategies::ByVary`
94
+
95
+ This strategy uses headers from `Vary` header to generate cache keys.
96
+ It also uses cache to store `Vary` headers mapped to the request URL.
97
+ This strategy is more suitable for caching private responses with the same URLs but different results for different users, like `https://api.github.com/user`.
98
+
99
+ *Note:* To automatically remove stale cache keys, you might want to use the `:expires_in` option.
100
+
101
+ ```ruby
102
+ store = ActiveSupport::Cache.lookup_store(:redis_cache_store, expires_in: 1.day, url: 'redis://localhost:6379/0')
103
+ client = Faraday.new do |builder|
104
+ builder.use :http_cache, store: store, strategy: Faraday::HttpCache::Strategies::ByVary
105
+ builder.adapter Faraday.default_adapter
106
+ end
107
+ ```
108
+
109
+ #### Custom strategies
110
+
111
+ You can write your own strategy by subclassing `Faraday::HttpCache::Strategies::BaseStrategy` and implementing `#write`, `#read` and `#delete` methods.
112
+
74
113
  ### Logging
75
114
 
76
- You can provide a `:logger` option that will be receive debug informations based on the middleware
115
+ You can provide a `:logger` option that will receive debug information based on the middleware
77
116
  operations:
78
117
 
79
118
  ```ruby
@@ -82,7 +121,7 @@ client = Faraday.new do |builder|
82
121
  builder.adapter Faraday.default_adapter
83
122
  end
84
123
 
85
- client.get('http://site/api/users')
124
+ client.get('https://site/api/users')
86
125
  # logs "HTTP Cache: [GET users] miss, store"
87
126
  ```
88
127
 
@@ -133,6 +172,7 @@ execute the files under the `examples` directory to see a sample of the middlewa
133
172
  ## What gets cached?
134
173
 
135
174
  The middleware will use the following headers to make caching decisions:
175
+ - Vary
136
176
  - Cache-Control
137
177
  - Age
138
178
  - Last-Modified
@@ -155,7 +195,7 @@ client = Faraday.new do |builder|
155
195
  builder.adapter Faraday.default_adapter
156
196
  end
157
197
 
158
- client.get('http://site/api/some-private-resource') # => will be cached
198
+ client.get('https://site/api/some-private-resource') # => will be cached
159
199
  ```
160
200
 
161
201
  ## License
@@ -163,4 +203,4 @@ client.get('http://site/api/some-private-resource') # => will be cached
163
203
  Copyright (c) 2012-2018 Plataformatec.
164
204
  Copyright (c) 2019 SourceLevel and contributors.
165
205
 
166
- [marshal]: http://www.ruby-doc.org/core-2.0/Marshal.html
206
+ [marshal]: https://www.ruby-doc.org/core-3.0/Marshal.html
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ class HttpCache < Faraday::Middleware
5
+ # @private
6
+ # A Hash based store to be used by strategies
7
+ # when a `store` is not provided for the middleware setup.
8
+ class MemoryStore
9
+ def initialize
10
+ @cache = {}
11
+ end
12
+
13
+ def read(key)
14
+ @cache[key]
15
+ end
16
+
17
+ def delete(key)
18
+ @cache.delete(key)
19
+ end
20
+
21
+ def write(key, value)
22
+ @cache[key] = value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,194 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'digest/sha1'
3
+ require 'faraday/http_cache/strategies/by_url'
5
4
 
6
5
  module Faraday
7
6
  class HttpCache < Faraday::Middleware
8
- # Internal: A wrapper around an ActiveSupport::CacheStore to store responses.
9
- #
10
- # Examples
11
- #
12
- # # Creates a new Storage using a MemCached backend from ActiveSupport.
13
- # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
14
- # Faraday::HttpCache::Storage.new(store: mem_cache_store)
15
- #
16
- # # Reuse some other instance of an ActiveSupport::Cache::Store object.
17
- # Faraday::HttpCache::Storage.new(store: Rails.cache)
18
- #
19
- # # Creates a new Storage using Marshal for serialization.
20
- # Faraday::HttpCache::Storage.new(store: Rails.cache, serializer: Marshal)
21
- class Storage
22
- # Public: Gets the underlying cache store object.
23
- attr_reader :cache
24
-
25
- # Internal: Initialize a new Storage object with a cache backend.
26
- #
27
- # :logger - A Logger object to be used to emit warnings.
28
- # :store - An cache store object that should respond to 'read',
29
- # 'write', and 'delete'.
30
- # :serializer - A serializer object that should respond to 'dump'
31
- # and 'load'.
32
- def initialize(store: nil, serializer: nil, logger: nil)
33
- @cache = store || MemoryStore.new
34
- @serializer = serializer || JSON
35
- @logger = logger
36
- assert_valid_store!
37
- end
38
-
39
- # Internal: Store a response inside the cache.
40
- #
41
- # request - A Faraday::HttpCache::::Request instance of the executed HTTP
42
- # request.
43
- # response - The Faraday::HttpCache::Response instance to be stored.
44
- #
45
- # Returns nothing.
46
- def write(request, response)
47
- key = cache_key_for(request.url)
48
- entry = serialize_entry(request.serializable_hash, response.serializable_hash)
49
-
50
- entries = cache.read(key) || []
51
- entries = entries.dup if entries.frozen?
52
-
53
- entries.reject! do |(cached_request, cached_response)|
54
- response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
55
- end
56
-
57
- entries << entry
58
-
59
- cache.write(key, entries)
60
- rescue ::Encoding::UndefinedConversionError => e
61
- warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
62
- raise e
63
- end
64
-
65
- # Internal: Attempt to retrieve an stored response that suits the incoming
66
- # HTTP request.
67
- #
68
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
69
- # request.
70
- # klass - The Class to be instantiated with the stored response.
71
- #
72
- # Returns an instance of 'klass'.
73
- def read(request, klass: Faraday::HttpCache::Response)
74
- cache_key = cache_key_for(request.url)
75
- entries = cache.read(cache_key)
76
- response = lookup_response(request, entries)
77
-
78
- if response
79
- klass.new(response)
80
- end
81
- end
82
-
83
- def delete(url)
84
- cache_key = cache_key_for(url)
85
- cache.delete(cache_key)
86
- end
87
-
88
- private
89
-
90
- # Internal: Retrieve a response Hash from the list of entries that match
91
- # the given request.
92
- #
93
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
94
- # request.
95
- # entries - An Array of pairs of Hashes (request, response).
96
- #
97
- # Returns a Hash or nil.
98
- def lookup_response(request, entries)
99
- if entries
100
- entries = entries.map { |entry| deserialize_entry(*entry) }
101
- _, response = entries.find { |req, res| response_matches?(request, req, res) }
102
- response
103
- end
104
- end
105
-
106
- # Internal: Check if a cached response and request matches the given
107
- # request.
108
- #
109
- # request - A Faraday::HttpCache::::Request instance of the
110
- # current HTTP request.
111
- # cached_request - The Hash of the request that was cached.
112
- # cached_response - The Hash of the response that was cached.
113
- #
114
- # Returns true or false.
115
- def response_matches?(request, cached_request, cached_response)
116
- request.method.to_s == cached_request[:method].to_s &&
117
- vary_matches?(cached_response, request, cached_request)
118
- end
119
-
120
- def vary_matches?(cached_response, request, cached_request)
121
- headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
122
- vary = headers['Vary'].to_s
123
-
124
- vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
125
- request.headers[header] == cached_request[:headers][header]
126
- end)
127
- end
128
-
129
- def serialize_entry(*objects)
130
- objects.map { |object| serialize_object(object) }
131
- end
132
-
133
- def serialize_object(object)
134
- @serializer.dump(object)
135
- end
136
-
137
- def deserialize_entry(*objects)
138
- objects.map { |object| deserialize_object(object) }
139
- end
140
-
141
- def deserialize_object(object)
142
- @serializer.load(object).each_with_object({}) do |(key, value), hash|
143
- hash[key.to_sym] = value
144
- end
145
- end
146
-
147
- # Internal: Computes the cache key for a specific request, taking in
148
- # account the current serializer to avoid cross serialization issues.
149
- #
150
- # url - The request URL.
151
- #
152
- # Returns a String.
153
- def cache_key_for(url)
154
- prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
155
- Digest::SHA1.hexdigest("#{prefix}#{url}")
156
- end
157
-
158
- # Internal: Checks if the given cache object supports the
159
- # expect API ('read' and 'write').
160
- #
161
- # Raises an 'ArgumentError'.
162
- #
163
- # Returns nothing.
164
- def assert_valid_store!
165
- unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
166
- raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
167
- end
168
- end
169
-
170
- def warn(message)
171
- @logger&.warn(message)
172
- end
173
- end
174
-
175
- # Internal: A Hash based store to be used by the 'Storage' class
176
- # when a 'store' is not provided for the middleware setup.
177
- class MemoryStore
178
- def initialize
179
- @cache = {}
180
- end
181
-
182
- def read(key)
183
- @cache[key]
184
- end
185
-
186
- def delete(key)
187
- @cache.delete(key)
188
- end
189
-
190
- def write(key, value)
191
- @cache[key] = value
7
+ # @deprecated Use Faraday::HttpCache::Strategies::ByUrl instead.
8
+ class Storage < Faraday::HttpCache::Strategies::ByUrl
9
+ def initialize(*)
10
+ Kernel.warn("Deprecated: #{self.class} is deprecated and will be removed in " \
11
+ 'the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.')
12
+ super
192
13
  end
193
14
  end
194
15
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'faraday/http_cache/memory_store'
5
+
6
+ module Faraday
7
+ class HttpCache < Faraday::Middleware
8
+ module Strategies
9
+ # Base class for all strategies.
10
+ # @abstract
11
+ #
12
+ # @example
13
+ #
14
+ # # Creates a new strategy using a MemCached backend from ActiveSupport.
15
+ # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
16
+ # Faraday::HttpCache::Strategies::ByVary.new(store: mem_cache_store)
17
+ #
18
+ # # Reuse some other instance of an ActiveSupport::Cache::Store object.
19
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache)
20
+ #
21
+ # # Creates a new strategy using Marshal for serialization.
22
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache, serializer: Marshal)
23
+ class BaseStrategy
24
+ # Returns the underlying cache store object.
25
+ attr_reader :cache
26
+
27
+ # @param [Hash] options the options to create a message with.
28
+ # @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
29
+ # store object that should respond to 'read', 'write', and 'delete'.
30
+ # @option options [#dump#load] :serializer - an object that should
31
+ # respond to 'dump' and 'load'.
32
+ # @option options [Logger, nil] :logger - an object to be used to emit warnings.
33
+ def initialize(options = {})
34
+ @cache = options[:store] || Faraday::HttpCache::MemoryStore.new
35
+ @serializer = options[:serializer] || JSON
36
+ @logger = options[:logger] || Logger.new(IO::NULL)
37
+ @cache_salt = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
38
+ assert_valid_store!
39
+ end
40
+
41
+ # Store a response inside the cache.
42
+ # @abstract
43
+ def write(_request, _response)
44
+ raise NotImplementedError, 'Implement this method in your strategy'
45
+ end
46
+
47
+ # Read a response from the cache.
48
+ # @abstract
49
+ def read(_request)
50
+ raise NotImplementedError, 'Implement this method in your strategy'
51
+ end
52
+
53
+ # Delete responses from the cache by the url.
54
+ # @abstract
55
+ def delete(_url)
56
+ raise NotImplementedError, 'Implement this method in your strategy'
57
+ end
58
+
59
+ private
60
+
61
+ # @private
62
+ # @raise [ArgumentError] if the cache object doesn't support the expect API.
63
+ def assert_valid_store!
64
+ unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
65
+ raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
66
+ end
67
+ end
68
+
69
+ def serialize_entry(*objects)
70
+ objects.map { |object| serialize_object(object) }
71
+ end
72
+
73
+ def serialize_object(object)
74
+ @serializer.dump(object)
75
+ end
76
+
77
+ def deserialize_entry(*objects)
78
+ objects.map { |object| deserialize_object(object) }
79
+ end
80
+
81
+ def deserialize_object(object)
82
+ @serializer.load(object).each_with_object({}) do |(key, value), hash|
83
+ hash[key.to_sym] = value
84
+ end
85
+ end
86
+
87
+ def warn(message)
88
+ @logger.warn(message)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'faraday/http_cache/strategies/base_strategy'
6
+
7
+ module Faraday
8
+ class HttpCache < Faraday::Middleware
9
+ module Strategies
10
+ # The original strategy by Faraday::HttpCache.
11
+ # Uses URL + HTTP method to generate cache keys.
12
+ class ByUrl < BaseStrategy
13
+ # Store a response inside the cache.
14
+ #
15
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
16
+ # @param [Faraday::HttpCache::Response] response - instance to be stored.
17
+ #
18
+ # @return [void]
19
+ def write(request, response)
20
+ key = cache_key_for(request.url)
21
+ entry = serialize_entry(request.serializable_hash, response.serializable_hash)
22
+ entries = cache.read(key) || []
23
+ entries = entries.dup if entries.frozen?
24
+ entries.reject! do |(cached_request, cached_response)|
25
+ response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
26
+ end
27
+
28
+ entries << entry
29
+
30
+ cache.write(key, entries)
31
+ rescue ::Encoding::UndefinedConversionError => e
32
+ warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
33
+ raise e
34
+ end
35
+
36
+ # Fetch a stored response that suits the incoming HTTP request or return nil.
37
+ #
38
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
39
+ #
40
+ # @return [Faraday::HttpCache::Response, nil]
41
+ def read(request)
42
+ cache_key = cache_key_for(request.url)
43
+ entries = cache.read(cache_key)
44
+ response = lookup_response(request, entries)
45
+ return nil unless response
46
+
47
+ Faraday::HttpCache::Response.new(response)
48
+ end
49
+
50
+ # @param [String] url – the url of a changed resource, will be used to invalidate the cache.
51
+ #
52
+ # @return [void]
53
+ def delete(url)
54
+ cache_key = cache_key_for(url)
55
+ cache.delete(cache_key)
56
+ end
57
+
58
+ private
59
+
60
+ # Retrieve a response Hash from the list of entries that match the given request.
61
+ #
62
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
63
+ # @param [Array<Array(Hash, Hash)>] entries - pairs of Hashes (request, response).
64
+ #
65
+ # @return [Hash, nil]
66
+ def lookup_response(request, entries)
67
+ if entries
68
+ entries = entries.map { |entry| deserialize_entry(*entry) }
69
+ _, response = entries.find { |req, res| response_matches?(request, req, res) }
70
+ response
71
+ end
72
+ end
73
+
74
+ # Check if a cached response and request matches the given request.
75
+ #
76
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
77
+ # @param [Hash] cached_request - a Hash of the request that was cached.
78
+ # @param [Hash] cached_response - a Hash of the response that was cached.
79
+ #
80
+ # @return [true, false]
81
+ def response_matches?(request, cached_request, cached_response)
82
+ request.method.to_s == cached_request[:method].to_s &&
83
+ vary_matches?(cached_response, request, cached_request)
84
+ end
85
+
86
+ # Check if the cached request matches the incoming
87
+ # request based on the Vary header of cached response.
88
+ #
89
+ # If Vary header is not present, the request is considered to match.
90
+ # If Vary header is '*', the request is considered to not match.
91
+ #
92
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
93
+ # @param [Hash] cached_request - a Hash of the request that was cached.
94
+ # @param [Hash] cached_response - a Hash of the response that was cached.
95
+ #
96
+ # @return [true, false]
97
+ def vary_matches?(cached_response, request, cached_request)
98
+ headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
99
+ vary = headers['Vary'].to_s
100
+
101
+ vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
102
+ request.headers[header] == cached_request[:headers][header]
103
+ end)
104
+ end
105
+
106
+ # Computes the cache key for a specific request, taking
107
+ # in account the current serializer to avoid cross serialization issues.
108
+ #
109
+ # @param [String] url - the request URL.
110
+ #
111
+ # @return [String]
112
+ def cache_key_for(url)
113
+ Digest::SHA1.hexdigest("#{@cache_salt}#{url}")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'faraday/http_cache/strategies/base_strategy'
6
+
7
+ module Faraday
8
+ class HttpCache < Faraday::Middleware
9
+ module Strategies
10
+ # This strategy uses headers from the Vary response header to generate cache keys.
11
+ # It also uses the index with Vary headers mapped to the request url.
12
+ # This strategy is more suitable for caching private responses with the same urls,
13
+ # like https://api.github.com/user.
14
+ #
15
+ # This strategy does not support #delete method to clear cache on unsafe methods.
16
+ class ByVary < BaseStrategy
17
+ # Store a response inside the cache.
18
+ #
19
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
20
+ # @param [Faraday::HttpCache::Response] response - instance to be stored.
21
+ #
22
+ # @return [void]
23
+ def write(request, response)
24
+ vary_cache_key = vary_cache_key_for(request)
25
+ headers = Faraday::Utils::Headers.new(response.payload[:response_headers])
26
+ vary = headers['Vary'].to_s
27
+ cache.write(vary_cache_key, vary)
28
+
29
+ response_cache_key = response_cache_key_for(request, vary)
30
+ entry = serialize_object(response.serializable_hash)
31
+ cache.write(response_cache_key, entry)
32
+ rescue ::Encoding::UndefinedConversionError => e
33
+ warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
34
+ raise e
35
+ end
36
+
37
+ # Fetch a stored response that suits the incoming HTTP request or return nil.
38
+ #
39
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
40
+ #
41
+ # @return [Faraday::HttpCache::Response, nil]
42
+ def read(request)
43
+ vary_cache_key = vary_cache_key_for(request)
44
+ vary = cache.read(vary_cache_key)
45
+ return nil if vary.nil? || vary == '*'
46
+
47
+ cache_key = response_cache_key_for(request, vary)
48
+ response = cache.read(cache_key)
49
+ return nil if response.nil?
50
+
51
+ Faraday::HttpCache::Response.new(deserialize_object(response))
52
+ end
53
+
54
+ # This strategy does not support #delete method to clear cache on unsafe methods.
55
+ # @return [void]
56
+ def delete(_url)
57
+ # do nothing since we can't find the key by url
58
+ end
59
+
60
+ private
61
+
62
+ # Computes the cache key for the index with Vary headers.
63
+ #
64
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
65
+ #
66
+ # @return [String]
67
+ def vary_cache_key_for(request)
68
+ method = request.method.to_s
69
+ Digest::SHA1.hexdigest("by_vary_index#{@cache_salt}#{method}#{request.url}")
70
+ end
71
+
72
+ # Computes the cache key for the response.
73
+ #
74
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
75
+ # @param [String] vary - the Vary header value.
76
+ #
77
+ # @return [String]
78
+ def response_cache_key_for(request, vary)
79
+ method = request.method.to_s
80
+ headers = vary.split(/[\s,]+/).map { |header| request.headers[header] }
81
+ Digest::SHA1.hexdigest("by_vary#{@cache_salt}#{method}#{request.url}#{headers.join}")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday/http_cache/strategies/by_url'
4
+ require 'faraday/http_cache/strategies/by_vary'
@@ -5,11 +5,12 @@ require 'faraday'
5
5
  require 'faraday/http_cache/storage'
6
6
  require 'faraday/http_cache/request'
7
7
  require 'faraday/http_cache/response'
8
+ require 'faraday/http_cache/strategies'
8
9
 
9
10
  module Faraday
10
11
  # Public: The middleware responsible for caching and serving responses.
11
- # The middleware use the provided configuration options to establish a
12
- # 'Faraday::HttpCache::Storage' to cache responses retrieved by the stack
12
+ # The middleware use the provided configuration options to establish on of
13
+ # 'Faraday::HttpCache::Strategies' to cache responses retrieved by the stack
13
14
  # adapter. If a stored response can be served again for a subsequent
14
15
  # request, the middleware will return the response instead of issuing a new
15
16
  # request to it's server. This middleware should be the last attached handler
@@ -91,7 +92,7 @@ module Faraday
91
92
  # Faraday::HttpCache.new(app, logger: my_logger)
92
93
  #
93
94
  # # Initialize the middleware with a logger and Marshal as a serializer
94
- # Faraday:HttpCache.new(app, logger: my_logger, serializer: Marshal)
95
+ # Faraday::HttpCache.new(app, logger: my_logger, serializer: Marshal)
95
96
  #
96
97
  # # Initialize the middleware with a FileStore at the 'tmp' dir.
97
98
  # store = ActiveSupport::Cache.lookup_store(:file_store, ['tmp'])
@@ -104,17 +105,14 @@ module Faraday
104
105
  super(app)
105
106
 
106
107
  options = options.dup
107
- @logger = options.delete(:logger)
108
+ @logger = options[:logger]
108
109
  @shared_cache = options.delete(:shared_cache) { true }
109
110
  @instrumenter = options.delete(:instrumenter)
110
111
  @instrument_name = options.delete(:instrument_name) { EVENT_NAME }
111
112
 
112
- store = options.delete(:store)
113
- serializer = options.delete(:serializer)
113
+ strategy = options.delete(:strategy) { Strategies::ByUrl }
114
114
 
115
- raise ArgumentError, "Unknown options: #{options.inspect}" unless options.empty?
116
-
117
- @storage = Storage.new(store: store, serializer: serializer, logger: @logger)
115
+ @strategy = strategy.new(**options)
118
116
  end
119
117
 
120
118
  # Public: Process the request into a duplicate of this instance to
@@ -159,9 +157,6 @@ module Faraday
159
157
  # Internal: Gets the request object created from the Faraday env Hash.
160
158
  attr_reader :request
161
159
 
162
- # Internal: Gets the storage instance associated with the middleware.
163
- attr_reader :storage
164
-
165
160
  private
166
161
 
167
162
  # Internal: Should this cache instance act like a "shared cache" according
@@ -190,7 +185,7 @@ module Faraday
190
185
  #
191
186
  # Returns the 'Faraday::Response' instance to be served.
192
187
  def process(env)
193
- entry = @storage.read(@request)
188
+ entry = @strategy.read(@request)
194
189
 
195
190
  return fetch(env) if entry.nil?
196
191
 
@@ -264,7 +259,7 @@ module Faraday
264
259
  def store(response)
265
260
  if shared_cache? ? response.cacheable_in_shared_cache? : response.cacheable_in_private_cache?
266
261
  trace :store
267
- @storage.write(@request, response)
262
+ @strategy.write(@request, response)
268
263
  else
269
264
  trace :uncacheable
270
265
  end
@@ -274,10 +269,10 @@ module Faraday
274
269
  headers = %w[Location Content-Location]
275
270
  headers.each do |header|
276
271
  url = response.headers[header]
277
- @storage.delete(url) if url
272
+ @strategy.delete(url) if url
278
273
  end
279
274
 
280
- @storage.delete(request.url)
275
+ @strategy.delete(request.url)
281
276
  trace :delete
282
277
  end
283
278
 
@@ -283,23 +283,13 @@ describe Faraday::HttpCache do
283
283
  expect(client.get('must-revalidate').body).to eq('1')
284
284
  end
285
285
 
286
- it 'raises an error when misconfigured' do
287
- expect {
288
- client = Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
289
- stack.use Faraday::HttpCache, i_have_no_idea: true
290
- end
291
-
292
- client.get('get')
293
- }.to raise_error(ArgumentError)
294
- end
295
-
296
286
  describe 'Configuration options' do
297
287
  let(:app) { double('it is an app!') }
298
288
 
299
289
  it 'uses the options to create a Cache Store' do
300
290
  store = double(read: nil, write: nil)
301
291
 
302
- expect(Faraday::HttpCache::Storage).to receive(:new).with(hash_including(store: store))
292
+ expect(Faraday::HttpCache::Strategies::ByUrl).to receive(:new).with(hash_including(store: store))
303
293
  Faraday::HttpCache.new(app, store: store)
304
294
  end
305
295
  end
data/spec/storage_spec.rb CHANGED
@@ -16,6 +16,21 @@ describe Faraday::HttpCache::Storage do
16
16
  let(:storage) { Faraday::HttpCache::Storage.new(store: cache) }
17
17
  subject { storage }
18
18
 
19
+ before do
20
+ allow(Kernel).to receive(:warn).with(
21
+ 'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
22
+ 'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
23
+ )
24
+ end
25
+
26
+ it 'creates strategy and warns about deprecation' do
27
+ expect(Kernel).to receive(:warn).with(
28
+ 'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
29
+ 'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
30
+ )
31
+ is_expected.to be_a_kind_of(Faraday::HttpCache::Strategies::ByUrl)
32
+ end
33
+
19
34
  describe 'Cache configuration' do
20
35
  it 'uses a MemoryStore by default' do
21
36
  expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
@@ -128,9 +143,10 @@ describe Faraday::HttpCache::Storage do
128
143
  end
129
144
 
130
145
  it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
146
+ current_time = Time.now
131
147
  headers = {
132
- 'Date' => (Time.now - 39).httpdate,
133
- 'Expires' => (Time.now + 40).httpdate
148
+ 'Date' => (current_time - 39).httpdate,
149
+ 'Expires' => (current_time + 40).httpdate
134
150
  }
135
151
 
136
152
  response = Faraday::HttpCache::Response.new(response_headers: headers)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Faraday::HttpCache::Strategies::BaseStrategy do
6
+ subject(:strategy) { described_class.new }
7
+
8
+ it 'uses a MemoryStore as a default store' do
9
+ expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
10
+ strategy
11
+ end
12
+
13
+ context 'when the given store is not valid' do
14
+ let(:store) { double(:wrong_store) }
15
+ subject(:strategy) { described_class.new(store: store) }
16
+
17
+ it 'raises an error' do
18
+ expect { strategy }.to raise_error(ArgumentError)
19
+ end
20
+ end
21
+
22
+ it 'raises an error when abstract methods are called' do
23
+ expect { strategy.write(nil, nil) }.to raise_error(NotImplementedError)
24
+ expect { strategy.read(nil) }.to raise_error(NotImplementedError)
25
+ expect { strategy.delete(nil) }.to raise_error(NotImplementedError)
26
+ end
27
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Faraday::HttpCache::Strategies::ByUrl do
6
+ let(:cache_key) { '6e3b941d0f7572291c777b3e48c04b74124a55d0' }
7
+ let(:request) do
8
+ env = { method: :get, url: 'http://test/index' }
9
+ double(env.merge(serializable_hash: env))
10
+ end
11
+
12
+ let(:response) { double(serializable_hash: { response_headers: {} }) }
13
+
14
+ let(:cache) { Faraday::HttpCache::MemoryStore.new }
15
+
16
+ let(:strategy) { described_class.new(store: cache) }
17
+ subject { strategy }
18
+
19
+ describe 'Cache configuration' do
20
+ it 'uses a MemoryStore by default' do
21
+ expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
22
+ described_class.new
23
+ end
24
+
25
+ it 'raises an error when the given store is not valid' do
26
+ wrong = double
27
+
28
+ expect {
29
+ described_class.new(store: wrong)
30
+ }.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+
34
+ describe 'storing responses' do
35
+ shared_examples 'A strategy with serialization' do
36
+ it 'writes the response object to the underlying cache' do
37
+ entry = [serializer.dump(request.serializable_hash), serializer.dump(response.serializable_hash)]
38
+ expect(cache).to receive(:write).with(cache_key, [entry])
39
+ subject.write(request, response)
40
+ end
41
+ end
42
+
43
+ context 'with the JSON serializer' do
44
+ let(:serializer) { JSON }
45
+ it_behaves_like 'A strategy with serialization'
46
+
47
+ context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
48
+ let(:response) do
49
+ body = String.new("\u2665").force_encoding('ASCII-8BIT')
50
+ double(:response, serializable_hash: { 'body' => body })
51
+ end
52
+
53
+ it 'raises and logs a warning' do
54
+ logger = double(:logger, warn: nil)
55
+ strategy = described_class.new(logger: logger)
56
+
57
+ expect {
58
+ strategy.write(request, response)
59
+ }.to raise_error(::Encoding::UndefinedConversionError)
60
+ expect(logger).to have_received(:warn).with(
61
+ 'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ context 'with the Marshal serializer' do
68
+ let(:cache_key) { '337d1e9c6c92423dd1c48a23054139058f97be40' }
69
+ let(:serializer) { Marshal }
70
+ let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
71
+
72
+ it_behaves_like 'A strategy with serialization'
73
+ end
74
+ end
75
+
76
+ describe 'reading responses' do
77
+ let(:strategy) { described_class.new(store: cache, serializer: serializer) }
78
+
79
+ shared_examples 'A strategy with serialization' do
80
+ it 'returns nil if the response is not cached' do
81
+ expect(subject.read(request)).to be_nil
82
+ end
83
+
84
+ it 'decodes a stored response' do
85
+ subject.write(request, response)
86
+
87
+ expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
88
+ end
89
+ end
90
+
91
+ context 'with the JSON serializer' do
92
+ let(:serializer) { JSON }
93
+
94
+ it_behaves_like 'A strategy with serialization'
95
+ end
96
+
97
+ context 'with the Marshal serializer' do
98
+ let(:serializer) { Marshal }
99
+
100
+ it_behaves_like 'A strategy with serialization'
101
+ end
102
+ end
103
+
104
+ describe 'deleting responses' do
105
+ it 'removes the entries from the cache of the given URL' do
106
+ subject.write(request, response)
107
+ subject.delete(request.url)
108
+ expect(subject.read(request)).to be_nil
109
+ end
110
+ end
111
+
112
+ describe 'remove age before caching and normalize max-age if non-zero age present' do
113
+ it 'is fresh if the response still has some time to live' do
114
+ headers = {
115
+ 'Age' => 6,
116
+ 'Cache-Control' => 'public, max-age=40',
117
+ 'Date' => (Time.now - 38).httpdate,
118
+ 'Expires' => (Time.now - 37).httpdate,
119
+ 'Last-Modified' => (Time.now - 300).httpdate
120
+ }
121
+ response = Faraday::HttpCache::Response.new(response_headers: headers)
122
+ expect(response).to be_fresh
123
+ subject.write(request, response)
124
+
125
+ cached_response = subject.read(request)
126
+ expect(cached_response.max_age).to eq(34)
127
+ expect(cached_response).not_to be_fresh
128
+ end
129
+
130
+ it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
131
+ headers = {
132
+ 'Date' => (Time.now - 39).httpdate,
133
+ 'Expires' => (Time.now + 40).httpdate
134
+ }
135
+
136
+ response = Faraday::HttpCache::Response.new(response_headers: headers)
137
+ expect(response).to be_fresh
138
+ subject.write(request, response)
139
+
140
+ sleep(1)
141
+ cached_response = subject.read(request)
142
+ expect(cached_response).not_to be_fresh
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Faraday::HttpCache::Strategies::ByVary do
6
+ let(:vary_index_cache_key) { '64896419583e8022efeb21d0ece6e266c0e58b59' }
7
+ let(:cache_key) { '25230d75622fffc4f4de8a6af69e6e3764f7eb6f' }
8
+ let(:vary) { '' }
9
+ let(:request) do
10
+ env = {method: :get, url: 'http://test/index'}
11
+ double(env.merge(serializable_hash: env))
12
+ end
13
+
14
+ let(:response_payload) { {response_headers: {'Vary' => vary}} }
15
+
16
+ let(:response) do
17
+ instance_double(Faraday::HttpCache::Response, payload: response_payload, serializable_hash: response_payload)
18
+ end
19
+
20
+ let(:cache) { Faraday::HttpCache::MemoryStore.new }
21
+
22
+ let(:strategy) { described_class.new(store: cache) }
23
+ subject { strategy }
24
+
25
+ describe 'storing responses' do
26
+ shared_examples 'A strategy with serialization' do
27
+ it 'writes the response object to the underlying cache' do
28
+ entry = serializer.dump(response.serializable_hash)
29
+ expect(cache).to receive(:write).with(vary_index_cache_key, vary)
30
+ expect(cache).to receive(:write).with(cache_key, entry)
31
+ subject.write(request, response)
32
+ end
33
+ end
34
+
35
+ context 'with the JSON serializer' do
36
+ let(:serializer) { JSON }
37
+ it_behaves_like 'A strategy with serialization'
38
+
39
+ context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
40
+ let(:response_payload) do
41
+ body = String.new("\u2665").force_encoding('ASCII-8BIT')
42
+ super().merge('body' => body)
43
+ end
44
+
45
+ it 'raises and logs a warning' do
46
+ logger = double(:logger, warn: nil)
47
+ strategy = described_class.new(logger: logger)
48
+
49
+ expect {
50
+ strategy.write(request, response)
51
+ }.to raise_error(::Encoding::UndefinedConversionError)
52
+ expect(logger).to have_received(:warn).with(
53
+ 'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
54
+ )
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'with the Marshal serializer' do
60
+ let(:vary_index_cache_key) { '6a7cb42440c10ef6edeb1826086a4d90b04103f0' }
61
+ let(:cache_key) { '45e0efd1a60d29ed69d6c6018dfcb96f58db89e0' }
62
+ let(:serializer) { Marshal }
63
+ let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
64
+
65
+ it_behaves_like 'A strategy with serialization'
66
+ end
67
+ end
68
+
69
+ describe 'reading responses' do
70
+ let(:strategy) { described_class.new(store: cache, serializer: serializer) }
71
+
72
+ shared_examples 'A strategy with serialization' do
73
+ it 'returns nil if the response is not cached' do
74
+ expect(subject.read(request)).to be_nil
75
+ end
76
+
77
+ it 'decodes a stored response' do
78
+ subject.write(request, response)
79
+
80
+ expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
81
+ end
82
+ end
83
+
84
+ context 'with the JSON serializer' do
85
+ let(:serializer) { JSON }
86
+
87
+ it_behaves_like 'A strategy with serialization'
88
+ end
89
+
90
+ context 'with the Marshal serializer' do
91
+ let(:serializer) { Marshal }
92
+
93
+ it_behaves_like 'A strategy with serialization'
94
+ end
95
+ end
96
+
97
+ describe 'deleting responses' do
98
+ it 'ignores delete method' do
99
+ subject.write(request, response)
100
+ subject.delete(request.url)
101
+ expect(subject.read(request)).not_to be_nil
102
+ end
103
+ end
104
+
105
+ describe 'remove age before caching and normalize max-age if non-zero age present' do
106
+ it 'is fresh if the response still has some time to live' do
107
+ headers = {
108
+ 'Age' => 6,
109
+ 'Cache-Control' => 'public, max-age=40',
110
+ 'Date' => (Time.now - 38).httpdate,
111
+ 'Expires' => (Time.now - 37).httpdate,
112
+ 'Last-Modified' => (Time.now - 300).httpdate
113
+ }
114
+ response = Faraday::HttpCache::Response.new(response_headers: headers)
115
+ expect(response).to be_fresh
116
+ subject.write(request, response)
117
+
118
+ cached_response = subject.read(request)
119
+ expect(cached_response.max_age).to eq(34)
120
+ expect(cached_response).not_to be_fresh
121
+ end
122
+
123
+ it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
124
+ headers = {
125
+ 'Date' => (Time.now - 39).httpdate,
126
+ 'Expires' => (Time.now + 40).httpdate
127
+ }
128
+
129
+ response = Faraday::HttpCache::Response.new(response_headers: headers)
130
+ expect(response).to be_fresh
131
+ subject.write(request, response)
132
+
133
+ sleep(1)
134
+ cached_response = subject.read(request)
135
+ expect(cached_response).not_to be_fresh
136
+ end
137
+ end
138
+ 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.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
+ - Lucas Mazza
7
8
  - George Guimarães
8
9
  - Gustavo Araujo
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2022-05-25 00:00:00.000000000 Z
13
+ date: 2022-06-08 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: faraday
@@ -37,9 +38,14 @@ files:
37
38
  - lib/faraday-http-cache.rb
38
39
  - lib/faraday/http_cache.rb
39
40
  - lib/faraday/http_cache/cache_control.rb
41
+ - lib/faraday/http_cache/memory_store.rb
40
42
  - lib/faraday/http_cache/request.rb
41
43
  - lib/faraday/http_cache/response.rb
42
44
  - lib/faraday/http_cache/storage.rb
45
+ - lib/faraday/http_cache/strategies.rb
46
+ - lib/faraday/http_cache/strategies/base_strategy.rb
47
+ - lib/faraday/http_cache/strategies/by_url.rb
48
+ - lib/faraday/http_cache/strategies/by_vary.rb
43
49
  - spec/binary_spec.rb
44
50
  - spec/cache_control_spec.rb
45
51
  - spec/http_cache_spec.rb
@@ -49,6 +55,9 @@ files:
49
55
  - spec/response_spec.rb
50
56
  - spec/spec_helper.rb
51
57
  - spec/storage_spec.rb
58
+ - spec/strategies/base_strategy_spec.rb
59
+ - spec/strategies/by_url_spec.rb
60
+ - spec/strategies/by_vary_spec.rb
52
61
  - spec/support/empty.png
53
62
  - spec/support/test_app.rb
54
63
  - spec/support/test_server.rb
@@ -72,21 +81,24 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
81
  - !ruby/object:Gem::Version
73
82
  version: '0'
74
83
  requirements: []
75
- rubygems_version: 3.0.3
84
+ rubygems_version: 3.2.3
76
85
  signing_key:
77
86
  specification_version: 4
78
87
  summary: A Faraday middleware that stores and validates cache expiration.
79
88
  test_files:
80
- - spec/spec_helper.rb
81
- - spec/validation_spec.rb
82
- - spec/json_spec.rb
89
+ - spec/binary_spec.rb
90
+ - spec/cache_control_spec.rb
91
+ - spec/http_cache_spec.rb
83
92
  - spec/instrumentation_spec.rb
93
+ - spec/json_spec.rb
94
+ - spec/request_spec.rb
95
+ - spec/response_spec.rb
96
+ - spec/spec_helper.rb
84
97
  - spec/storage_spec.rb
85
- - spec/http_cache_spec.rb
86
- - spec/binary_spec.rb
87
- - spec/support/test_app.rb
98
+ - spec/strategies/base_strategy_spec.rb
99
+ - spec/strategies/by_url_spec.rb
100
+ - spec/strategies/by_vary_spec.rb
88
101
  - spec/support/empty.png
102
+ - spec/support/test_app.rb
89
103
  - spec/support/test_server.rb
90
- - spec/request_spec.rb
91
- - spec/cache_control_spec.rb
92
- - spec/response_spec.rb
104
+ - spec/validation_spec.rb