faraday-http-cache 2.2.0 → 2.4.1

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
- SHA1:
3
- metadata.gz: f19d02b8f8069a385636da706ab6ac31f03ed794
4
- data.tar.gz: 86052f071b2b1cee24dc7fa8af957cd59fd95244
2
+ SHA256:
3
+ metadata.gz: 6f9734c5eda496aee6fc6d189e27f9e39b43600ffb467976649ad3f7046a8efc
4
+ data.tar.gz: 1a4d2d137c3ec0a97790cc4b5bcb706f7e0c2af0d0700e0ae03dfc79f5e2bdba
5
5
  SHA512:
6
- metadata.gz: 3c14cd6befa55a2df9e08c24df11fb92a437384e0490a705193506905ac1532fcb43e6439867bc8243b35dafd1610a965fa5a3bbf6feabfe4321544e7cbe1664
7
- data.tar.gz: 764cc0374795c32d68bb64399698450c367916b7ef69612d07a91b9619c893fa5b35b65cb76930e9fb129e3d53694bde6a11a99c4ee14238b9beac4f1dd1e2ea
6
+ metadata.gz: f93240786d8c3b8012cce14545e2f0f34f1bb12811da4edc549ad145284712b59380dac02f91ed6170d19af7af220e48d905a31a1159129d2b32b45799251df6
7
+ data.tar.gz: 8b2480bebc178003743ca2e0535966ad617349ef7ead2f7b5907f00d4580ba990332472d2ac393dc6b5bd79b787d8d560e5d772f9bfc1faca76ac6b34a5061d7
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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Faraday
3
4
  class HttpCache < Faraday::Middleware
4
5
  # Internal: A class to represent the 'Cache-Control' header options.
@@ -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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Faraday
3
4
  class HttpCache < Faraday::Middleware
4
5
  # Internal: A class to represent a request
@@ -24,6 +25,7 @@ module Faraday
24
25
  def cacheable?
25
26
  return false if method != :get && method != :head
26
27
  return false if cache_control.no_store?
28
+
27
29
  true
28
30
  end
29
31
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'time'
3
4
  require 'faraday/http_cache/cache_control'
4
5
 
@@ -204,7 +205,7 @@ module Faraday
204
205
  # Returns nothing.
205
206
  def ensure_date_header!
206
207
  date
207
- rescue
208
+ rescue StandardError
208
209
  headers['Date'] = @now.httpdate
209
210
  end
210
211
 
@@ -1,193 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'json'
3
- require 'digest/sha1'
2
+
3
+ require 'faraday/http_cache/strategies/by_url'
4
4
 
5
5
  module Faraday
6
6
  class HttpCache < Faraday::Middleware
7
- # Internal: A wrapper around an ActiveSupport::CacheStore to store responses.
8
- #
9
- # Examples
10
- #
11
- # # Creates a new Storage using a MemCached backend from ActiveSupport.
12
- # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
13
- # Faraday::HttpCache::Storage.new(store: mem_cache_store)
14
- #
15
- # # Reuse some other instance of an ActiveSupport::Cache::Store object.
16
- # Faraday::HttpCache::Storage.new(store: Rails.cache)
17
- #
18
- # # Creates a new Storage using Marshal for serialization.
19
- # Faraday::HttpCache::Storage.new(store: Rails.cache, serializer: Marshal)
20
- class Storage
21
- # Public: Gets the underlying cache store object.
22
- attr_reader :cache
23
-
24
- # Internal: Initialize a new Storage object with a cache backend.
25
- #
26
- # :logger - A Logger object to be used to emit warnings.
27
- # :store - An cache store object that should respond to 'read',
28
- # 'write', and 'delete'.
29
- # :serializer - A serializer object that should respond to 'dump'
30
- # and 'load'.
31
- def initialize(store: nil, serializer: nil, logger: nil)
32
- @cache = store || MemoryStore.new
33
- @serializer = serializer || JSON
34
- @logger = logger
35
- assert_valid_store!
36
- end
37
-
38
- # Internal: Store a response inside the cache.
39
- #
40
- # request - A Faraday::HttpCache::::Request instance of the executed HTTP
41
- # request.
42
- # response - The Faraday::HttpCache::Response instance to be stored.
43
- #
44
- # Returns nothing.
45
- def write(request, response)
46
- key = cache_key_for(request.url)
47
- entry = serialize_entry(request.serializable_hash, response.serializable_hash)
48
-
49
- entries = cache.read(key) || []
50
- entries = entries.dup if entries.frozen?
51
-
52
- entries.reject! do |(cached_request, cached_response)|
53
- response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
54
- end
55
-
56
- entries << entry
57
-
58
- cache.write(key, entries)
59
- rescue ::Encoding::UndefinedConversionError => e
60
- warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
61
- raise e
62
- end
63
-
64
- # Internal: Attempt to retrieve an stored response that suits the incoming
65
- # HTTP request.
66
- #
67
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
68
- # request.
69
- # klass - The Class to be instantiated with the stored response.
70
- #
71
- # Returns an instance of 'klass'.
72
- def read(request, klass: Faraday::HttpCache::Response)
73
- cache_key = cache_key_for(request.url)
74
- entries = cache.read(cache_key)
75
- response = lookup_response(request, entries)
76
-
77
- if response
78
- klass.new(response)
79
- end
80
- end
81
-
82
- def delete(url)
83
- cache_key = cache_key_for(url)
84
- cache.delete(cache_key)
85
- end
86
-
87
- private
88
-
89
- # Internal: Retrieve a response Hash from the list of entries that match
90
- # the given request.
91
- #
92
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
93
- # request.
94
- # entries - An Array of pairs of Hashes (request, response).
95
- #
96
- # Returns a Hash or nil.
97
- def lookup_response(request, entries)
98
- if entries
99
- entries = entries.map { |entry| deserialize_entry(*entry) }
100
- _, response = entries.find { |req, res| response_matches?(request, req, res) }
101
- response
102
- end
103
- end
104
-
105
- # Internal: Check if a cached response and request matches the given
106
- # request.
107
- #
108
- # request - A Faraday::HttpCache::::Request instance of the
109
- # current HTTP request.
110
- # cached_request - The Hash of the request that was cached.
111
- # cached_response - The Hash of the response that was cached.
112
- #
113
- # Returns true or false.
114
- def response_matches?(request, cached_request, cached_response)
115
- request.method.to_s == cached_request[:method].to_s &&
116
- vary_matches?(cached_response, request, cached_request)
117
- end
118
-
119
- def vary_matches?(cached_response, request, cached_request)
120
- headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
121
- vary = headers['Vary'].to_s
122
-
123
- vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
124
- request.headers[header] == cached_request[:headers][header]
125
- end)
126
- end
127
-
128
- def serialize_entry(*objects)
129
- objects.map { |object| serialize_object(object) }
130
- end
131
-
132
- def serialize_object(object)
133
- @serializer.dump(object)
134
- end
135
-
136
- def deserialize_entry(*objects)
137
- objects.map { |object| deserialize_object(object) }
138
- end
139
-
140
- def deserialize_object(object)
141
- @serializer.load(object).each_with_object({}) do |(key, value), hash|
142
- hash[key.to_sym] = value
143
- end
144
- end
145
-
146
- # Internal: Computes the cache key for a specific request, taking in
147
- # account the current serializer to avoid cross serialization issues.
148
- #
149
- # url - The request URL.
150
- #
151
- # Returns a String.
152
- def cache_key_for(url)
153
- prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
154
- Digest::SHA1.hexdigest("#{prefix}#{url}")
155
- end
156
-
157
- # Internal: Checks if the given cache object supports the
158
- # expect API ('read' and 'write').
159
- #
160
- # Raises an 'ArgumentError'.
161
- #
162
- # Returns nothing.
163
- def assert_valid_store!
164
- unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
165
- raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
166
- end
167
- end
168
-
169
- def warn(message)
170
- @logger.warn(message) if @logger
171
- end
172
- end
173
-
174
- # Internal: A Hash based store to be used by the 'Storage' class
175
- # when a 'store' is not provided for the middleware setup.
176
- class MemoryStore
177
- def initialize
178
- @cache = {}
179
- end
180
-
181
- def read(key)
182
- @cache[key]
183
- end
184
-
185
- def delete(key)
186
- @cache.delete(key)
187
- end
188
-
189
- def write(key, value)
190
- @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
191
13
  end
192
14
  end
193
15
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'logger'
5
+ require 'faraday/http_cache/memory_store'
6
+
7
+ module Faraday
8
+ class HttpCache < Faraday::Middleware
9
+ module Strategies
10
+ # Base class for all strategies.
11
+ # @abstract
12
+ #
13
+ # @example
14
+ #
15
+ # # Creates a new strategy using a MemCached backend from ActiveSupport.
16
+ # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
17
+ # Faraday::HttpCache::Strategies::ByVary.new(store: mem_cache_store)
18
+ #
19
+ # # Reuse some other instance of an ActiveSupport::Cache::Store object.
20
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache)
21
+ #
22
+ # # Creates a new strategy using Marshal for serialization.
23
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache, serializer: Marshal)
24
+ class BaseStrategy
25
+ # Returns the underlying cache store object.
26
+ attr_reader :cache
27
+
28
+ # @param [Hash] options the options to create a message with.
29
+ # @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
30
+ # store object that should respond to 'read', 'write', and 'delete'.
31
+ # @option options [#dump#load] :serializer - an object that should
32
+ # respond to 'dump' and 'load'.
33
+ # @option options [Logger, nil] :logger - an object to be used to emit warnings.
34
+ def initialize(options = {})
35
+ @cache = options[:store] || Faraday::HttpCache::MemoryStore.new
36
+ @serializer = options[:serializer] || JSON
37
+ @logger = options[:logger] || Logger.new(IO::NULL)
38
+ @cache_salt = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
39
+ assert_valid_store!
40
+ end
41
+
42
+ # Store a response inside the cache.
43
+ # @abstract
44
+ def write(_request, _response)
45
+ raise NotImplementedError, 'Implement this method in your strategy'
46
+ end
47
+
48
+ # Read a response from the cache.
49
+ # @abstract
50
+ def read(_request)
51
+ raise NotImplementedError, 'Implement this method in your strategy'
52
+ end
53
+
54
+ # Delete responses from the cache by the url.
55
+ # @abstract
56
+ def delete(_url)
57
+ raise NotImplementedError, 'Implement this method in your strategy'
58
+ end
59
+
60
+ private
61
+
62
+ # @private
63
+ # @raise [ArgumentError] if the cache object doesn't support the expect API.
64
+ def assert_valid_store!
65
+ unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
66
+ raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
67
+ end
68
+ end
69
+
70
+ def serialize_entry(*objects)
71
+ objects.map { |object| serialize_object(object) }
72
+ end
73
+
74
+ def serialize_object(object)
75
+ @serializer.dump(object)
76
+ end
77
+
78
+ def deserialize_entry(*objects)
79
+ objects.map { |object| deserialize_object(object) }
80
+ end
81
+
82
+ def deserialize_object(object)
83
+ @serializer.load(object).each_with_object({}) do |(key, value), hash|
84
+ hash[key.to_sym] = value
85
+ end
86
+ end
87
+
88
+ def warn(message)
89
+ @logger.warn(message)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ 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,]+/).uniq.sort.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