httpx 1.6.3 → 1.7.1
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/doc/release_notes/0_11_0.md +3 -3
- data/doc/release_notes/1_6_3.md +2 -2
- data/doc/release_notes/1_7_0.md +149 -0
- data/doc/release_notes/1_7_1.md +21 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/webmock.rb +18 -9
- data/lib/httpx/altsvc.rb +4 -2
- data/lib/httpx/connection/http1.rb +9 -9
- data/lib/httpx/connection/http2.rb +2 -0
- data/lib/httpx/connection.rb +7 -9
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/tcp.rb +1 -1
- data/lib/httpx/loggable.rb +2 -0
- data/lib/httpx/options.rb +118 -22
- data/lib/httpx/parser/http1.rb +1 -0
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +113 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
- data/lib/httpx/plugins/cookies/cookie.rb +1 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -5
- data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +156 -57
- data/lib/httpx/plugins/persistent.rb +3 -5
- data/lib/httpx/plugins/proxy/http.rb +0 -4
- data/lib/httpx/plugins/proxy.rb +3 -1
- data/lib/httpx/plugins/query.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +20 -15
- data/lib/httpx/plugins/response_cache.rb +3 -7
- data/lib/httpx/plugins/retries.rb +60 -24
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +60 -9
- data/lib/httpx/plugins/stream_bidi.rb +84 -16
- data/lib/httpx/pool.rb +12 -3
- data/lib/httpx/request/body.rb +1 -1
- data/lib/httpx/request.rb +10 -1
- data/lib/httpx/resolver/cache/base.rb +136 -0
- data/lib/httpx/resolver/cache/memory.rb +42 -0
- data/lib/httpx/resolver/cache.rb +18 -0
- data/lib/httpx/resolver/https.rb +74 -20
- data/lib/httpx/resolver/multi.rb +10 -2
- data/lib/httpx/resolver/native.rb +32 -6
- data/lib/httpx/resolver/resolver.rb +3 -3
- data/lib/httpx/resolver.rb +36 -114
- data/lib/httpx/response/body.rb +5 -3
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +14 -3
- data/lib/httpx/session.rb +6 -6
- data/lib/httpx/timers.rb +6 -12
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/gzip.rb +7 -2
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +5 -5
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +17 -9
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/utils.rb +13 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +9 -3
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +1 -3
- data/sig/loggable.rbs +1 -1
- data/sig/options.rbs +12 -4
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +37 -4
- data/sig/plugins/basic_auth.rbs +3 -3
- data/sig/plugins/digest_auth.rbs +2 -4
- data/sig/plugins/fiber_concurrency.rbs +6 -0
- data/sig/plugins/ntlm_auth.rbs +2 -2
- data/sig/plugins/oauth.rbs +44 -15
- data/sig/plugins/rate_limiter.rbs +4 -2
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +12 -4
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/resolver/cache/base.rbs +28 -0
- data/sig/resolver/cache/memory.rbs +13 -0
- data/sig/resolver/cache.rbs +16 -0
- data/sig/resolver/https.rbs +24 -0
- data/sig/resolver/multi.rbs +8 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver.rbs +5 -20
- data/sig/response.rbs +3 -0
- data/sig/session.rbs +3 -5
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- data/sig/utils.rbs +2 -0
- metadata +11 -1
data/lib/httpx/plugins/oauth.rb
CHANGED
|
@@ -2,21 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module HTTPX
|
|
4
4
|
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for managing an OAuth Session associated with the given session.
|
|
7
|
+
#
|
|
8
|
+
# The scope of OAuth support is limited to the `client_crendentials` and `refresh_token` grants.
|
|
5
9
|
#
|
|
6
10
|
# https://gitlab.com/os85/httpx/wikis/OAuth
|
|
7
11
|
#
|
|
8
12
|
module OAuth
|
|
9
13
|
class << self
|
|
10
|
-
def load_dependencies(
|
|
14
|
+
def load_dependencies(klass)
|
|
11
15
|
require_relative "auth/basic"
|
|
16
|
+
klass.plugin(:auth)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def subplugins
|
|
20
|
+
{
|
|
21
|
+
retries: OAuthRetries,
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extra_options(options)
|
|
26
|
+
options.merge(auth_header_type: "Bearer")
|
|
12
27
|
end
|
|
13
28
|
end
|
|
14
29
|
|
|
15
30
|
SUPPORTED_GRANT_TYPES = %w[client_credentials refresh_token].freeze
|
|
16
31
|
SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
|
|
17
32
|
|
|
33
|
+
# Implements the bulk of functionality and maintains the state associated with the
|
|
34
|
+
# management of the the lifecycle of an OAuth session.
|
|
18
35
|
class OAuthSession
|
|
19
|
-
attr_reader :
|
|
36
|
+
attr_reader :access_token, :refresh_token
|
|
20
37
|
|
|
21
38
|
def initialize(
|
|
22
39
|
issuer:,
|
|
@@ -27,7 +44,6 @@ module HTTPX
|
|
|
27
44
|
scope: nil,
|
|
28
45
|
audience: nil,
|
|
29
46
|
token_endpoint: nil,
|
|
30
|
-
response_type: nil,
|
|
31
47
|
grant_type: nil,
|
|
32
48
|
token_endpoint_auth_method: nil
|
|
33
49
|
)
|
|
@@ -35,7 +51,6 @@ module HTTPX
|
|
|
35
51
|
@client_id = client_id
|
|
36
52
|
@client_secret = client_secret
|
|
37
53
|
@token_endpoint = URI(token_endpoint) if token_endpoint
|
|
38
|
-
@response_type = response_type
|
|
39
54
|
@scope = case scope
|
|
40
55
|
when String
|
|
41
56
|
scope.split
|
|
@@ -47,6 +62,8 @@ module HTTPX
|
|
|
47
62
|
@refresh_token = refresh_token
|
|
48
63
|
@token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
|
|
49
64
|
@grant_type = grant_type || (@refresh_token ? "refresh_token" : "client_credentials")
|
|
65
|
+
@access_token = access_token
|
|
66
|
+
@refresh_token = refresh_token
|
|
50
67
|
|
|
51
68
|
unless @token_endpoint_auth_method.nil? || SUPPORTED_AUTH_METHODS.include?(@token_endpoint_auth_method)
|
|
52
69
|
raise Error, "#{@token_endpoint_auth_method} is not a supported auth method"
|
|
@@ -57,28 +74,75 @@ module HTTPX
|
|
|
57
74
|
raise Error, "#{@grant_type} is not a supported grant type"
|
|
58
75
|
end
|
|
59
76
|
|
|
77
|
+
# returns the URL where to request access and refresh tokens from.
|
|
60
78
|
def token_endpoint
|
|
61
79
|
@token_endpoint || "#{@issuer}/token"
|
|
62
80
|
end
|
|
63
81
|
|
|
82
|
+
# returns the oauth-documented authorization method to use when requesting a token.
|
|
64
83
|
def token_endpoint_auth_method
|
|
65
84
|
@token_endpoint_auth_method || "client_secret_basic"
|
|
66
85
|
end
|
|
67
86
|
|
|
68
|
-
def
|
|
69
|
-
|
|
87
|
+
def reset!
|
|
88
|
+
@access_token = nil
|
|
89
|
+
end
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
# when not available, it uses the +http+ object to request new access and refresh tokens.
|
|
92
|
+
def fetch_access_token(http)
|
|
93
|
+
return access_token if access_token
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
load(http)
|
|
96
|
+
|
|
97
|
+
# always prefer refresh token grant if a refresh token is available
|
|
98
|
+
grant_type = @refresh_token ? "refresh_token" : @grant_type
|
|
99
|
+
|
|
100
|
+
headers = {} # : Hash[String ,String]
|
|
101
|
+
form_post = {
|
|
102
|
+
"grant_type" => @grant_type,
|
|
103
|
+
"scope" => Array(@scope).join(" "),
|
|
104
|
+
"audience" => @audience,
|
|
105
|
+
}.compact
|
|
106
|
+
|
|
107
|
+
# auth
|
|
108
|
+
case token_endpoint_auth_method
|
|
109
|
+
when "client_secret_post"
|
|
110
|
+
form_post["client_id"] = @client_id
|
|
111
|
+
form_post["client_secret"] = @client_secret
|
|
112
|
+
when "client_secret_basic"
|
|
113
|
+
headers["authorization"] = Authentication::Basic.new(@client_id, @client_secret).authenticate
|
|
78
114
|
end
|
|
79
|
-
|
|
115
|
+
|
|
116
|
+
case grant_type
|
|
117
|
+
when "client_credentials"
|
|
118
|
+
# do nothing
|
|
119
|
+
when "refresh_token"
|
|
120
|
+
raise Error, "cannot use the `\"refresh_token\"` grant type without a refresh token" unless refresh_token
|
|
121
|
+
|
|
122
|
+
form_post["refresh_token"] = refresh_token
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# POST /token
|
|
126
|
+
token_request = http.build_request("POST", token_endpoint, headers: headers, form: form_post)
|
|
127
|
+
|
|
128
|
+
token_request.headers.delete("authorization") unless token_endpoint_auth_method == "client_secret_basic"
|
|
129
|
+
|
|
130
|
+
token_response = http.skip_auth_header { http.request(token_request) }
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
token_response.raise_for_status
|
|
134
|
+
rescue HTTPError => e
|
|
135
|
+
@refresh_token = nil if e.response.status == 401 && (grant_type == "refresh_token")
|
|
136
|
+
raise e
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
payload = token_response.json
|
|
140
|
+
|
|
141
|
+
@refresh_token = payload["refresh_token"] || @refresh_token
|
|
142
|
+
@access_token = payload["access_token"]
|
|
80
143
|
end
|
|
81
144
|
|
|
145
|
+
# TODO: remove this after deprecating the `:oauth_session` option
|
|
82
146
|
def merge(other)
|
|
83
147
|
obj = dup
|
|
84
148
|
|
|
@@ -97,12 +161,36 @@ module HTTPX
|
|
|
97
161
|
end
|
|
98
162
|
obj
|
|
99
163
|
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# uses +http+ to fetch for the oauth server metadata.
|
|
168
|
+
def load(http)
|
|
169
|
+
return if @grant_type && @scope
|
|
170
|
+
|
|
171
|
+
metadata = http.skip_auth_header { http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json }
|
|
172
|
+
|
|
173
|
+
@token_endpoint = metadata["token_endpoint"]
|
|
174
|
+
@scope = metadata["scopes_supported"]
|
|
175
|
+
@grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
|
|
176
|
+
@token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
|
|
177
|
+
SUPPORTED_AUTH_METHODS.include?(am)
|
|
178
|
+
end
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
100
181
|
end
|
|
101
182
|
|
|
183
|
+
# adds support for the following options:
|
|
184
|
+
#
|
|
185
|
+
# :oauth_options :: an hash of options to be used during session management.
|
|
186
|
+
# check the parameters to initialize the OAuthSession class.
|
|
102
187
|
module OptionsMethods
|
|
103
188
|
private
|
|
104
189
|
|
|
105
190
|
def option_oauth_session(value)
|
|
191
|
+
warn "DEPRECATION WARNING: option `:oauth_session` is deprecated. " \
|
|
192
|
+
"Use `:oauth_options` instead."
|
|
193
|
+
|
|
106
194
|
case value
|
|
107
195
|
when Hash
|
|
108
196
|
OAuthSession.new(**value)
|
|
@@ -112,69 +200,80 @@ module HTTPX
|
|
|
112
200
|
raise TypeError, ":oauth_session must be a #{OAuthSession}"
|
|
113
201
|
end
|
|
114
202
|
end
|
|
115
|
-
end
|
|
116
203
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
def option_oauth_options(value)
|
|
205
|
+
value = Hash[value] unless value.is_a?(Hash)
|
|
206
|
+
value
|
|
120
207
|
end
|
|
208
|
+
end
|
|
121
209
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
oauth_session.load(self)
|
|
126
|
-
|
|
127
|
-
grant_type = oauth_session.grant_type
|
|
210
|
+
module InstanceMethods
|
|
211
|
+
attr_reader :oauth_session
|
|
212
|
+
protected :oauth_session
|
|
128
213
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"grant_type" => grant_type,
|
|
132
|
-
"scope" => Array(oauth_session.scope).join(" "),
|
|
133
|
-
"audience" => oauth_session.audience,
|
|
134
|
-
}.compact
|
|
214
|
+
def initialize(*)
|
|
215
|
+
super
|
|
135
216
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
form_post["client_secret"] = oauth_session.client_secret
|
|
141
|
-
when "client_secret_basic"
|
|
142
|
-
headers["authorization"] = Authentication::Basic.new(oauth_session.client_id, oauth_session.client_secret).authenticate
|
|
217
|
+
@oauth_session = if @options.oauth_options
|
|
218
|
+
OAuthSession.new(**@options.oauth_options)
|
|
219
|
+
elsif @options.oauth_session
|
|
220
|
+
@oauth_session = @options.oauth_session.dup
|
|
143
221
|
end
|
|
222
|
+
end
|
|
144
223
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
224
|
+
def initialize_dup(other)
|
|
225
|
+
super
|
|
226
|
+
@oauth_session = other.instance_variable_get(:@oauth_session).dup
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def oauth_auth(**args)
|
|
230
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
231
|
+
"Use `with(oauth_options: options)` instead."
|
|
151
232
|
|
|
152
|
-
|
|
153
|
-
|
|
233
|
+
with(oauth_options: args)
|
|
234
|
+
end
|
|
154
235
|
|
|
155
|
-
|
|
156
|
-
|
|
236
|
+
# will eagerly negotiate new oauth tokens with the issuer
|
|
237
|
+
def refresh_oauth_tokens!
|
|
238
|
+
return unless @oauth_session
|
|
157
239
|
|
|
158
|
-
|
|
240
|
+
@oauth_session.reset!
|
|
241
|
+
@oauth_session.fetch_access_token(self)
|
|
242
|
+
end
|
|
159
243
|
|
|
160
|
-
|
|
161
|
-
|
|
244
|
+
# TODO: deprecate
|
|
245
|
+
def with_access_token
|
|
246
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
247
|
+
"The session will automatically handle token lifecycles for you."
|
|
162
248
|
|
|
163
|
-
|
|
249
|
+
other_session = dup # : instance
|
|
250
|
+
oauth_session = other_session.oauth_session
|
|
251
|
+
oauth_session.fetch_access_token(other_session)
|
|
252
|
+
other_session
|
|
164
253
|
end
|
|
165
254
|
|
|
166
|
-
|
|
167
|
-
request = super
|
|
255
|
+
private
|
|
168
256
|
|
|
169
|
-
|
|
257
|
+
def generate_auth_token
|
|
258
|
+
return unless @oauth_session
|
|
170
259
|
|
|
171
|
-
oauth_session
|
|
260
|
+
@oauth_session.fetch_access_token(self)
|
|
261
|
+
end
|
|
172
262
|
|
|
173
|
-
|
|
263
|
+
def dynamic_auth_token?(_)
|
|
264
|
+
@oauth_session
|
|
265
|
+
end
|
|
266
|
+
end
|
|
174
267
|
|
|
175
|
-
|
|
268
|
+
module OAuthRetries
|
|
269
|
+
module InstanceMethods
|
|
270
|
+
private
|
|
176
271
|
|
|
177
|
-
|
|
272
|
+
def prepare_to_retry(_request, response)
|
|
273
|
+
@oauth_session.reset! if @oauth_session
|
|
274
|
+
|
|
275
|
+
super
|
|
276
|
+
end
|
|
178
277
|
end
|
|
179
278
|
end
|
|
180
279
|
end
|
|
@@ -55,10 +55,8 @@ module HTTPX
|
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
-
def
|
|
58
|
+
def retryable_request?(request, response, *)
|
|
59
59
|
super || begin
|
|
60
|
-
response = request.response
|
|
61
|
-
|
|
62
60
|
return false unless response && response.is_a?(ErrorResponse)
|
|
63
61
|
|
|
64
62
|
error = response.error
|
|
@@ -67,13 +65,13 @@ module HTTPX
|
|
|
67
65
|
end
|
|
68
66
|
end
|
|
69
67
|
|
|
70
|
-
def retryable_error?(ex)
|
|
68
|
+
def retryable_error?(ex, options)
|
|
71
69
|
super &&
|
|
72
70
|
# under the persistent plugin rules, requests are only retried for connection related errors,
|
|
73
71
|
# which do not include request timeout related errors. This only gets overriden if the end user
|
|
74
72
|
# manually changed +:max_retries+ to something else, which means it is aware of the
|
|
75
73
|
# consequences.
|
|
76
|
-
(!ex.is_a?(RequestTimeoutError) ||
|
|
74
|
+
(!ex.is_a?(RequestTimeoutError) || options.max_retries != 1)
|
|
77
75
|
end
|
|
78
76
|
end
|
|
79
77
|
end
|
data/lib/httpx/plugins/proxy.rb
CHANGED
data/lib/httpx/plugins/query.rb
CHANGED
|
@@ -12,22 +12,11 @@ module HTTPX
|
|
|
12
12
|
# https://gitlab.com/os85/httpx/wikis/Rate-Limiter
|
|
13
13
|
#
|
|
14
14
|
module RateLimiter
|
|
15
|
-
|
|
16
|
-
RATE_LIMIT_CODES = [429, 503].freeze
|
|
17
|
-
|
|
18
|
-
def configure(klass)
|
|
19
|
-
klass.plugin(:retries,
|
|
20
|
-
retry_change_requests: true,
|
|
21
|
-
retry_on: method(:retry_on_rate_limited_response),
|
|
22
|
-
retry_after: method(:retry_after_rate_limit))
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def retry_on_rate_limited_response(response)
|
|
26
|
-
return false unless response.is_a?(Response)
|
|
15
|
+
RATE_LIMIT_CODES = [429, 503].freeze
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
class << self
|
|
18
|
+
def load_dependencies(klass)
|
|
19
|
+
klass.plugin(:retries, retry_after: method(:retry_after_rate_limit))
|
|
31
20
|
end
|
|
32
21
|
|
|
33
22
|
# Servers send the "Retry-After" header field to indicate how long the
|
|
@@ -48,6 +37,22 @@ module HTTPX
|
|
|
48
37
|
Utils.parse_retry_after(retry_after)
|
|
49
38
|
end
|
|
50
39
|
end
|
|
40
|
+
|
|
41
|
+
module InstanceMethods
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def retryable_request?(request, response, options)
|
|
45
|
+
super || rate_limit_error?(response)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def retryable_response?(response, options)
|
|
49
|
+
rate_limit_error?(response) || super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def rate_limit_error?(response)
|
|
53
|
+
response.is_a?(Response) && RATE_LIMIT_CODES.include?(response.status)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
register_plugin :rate_limiter, RateLimiter
|
|
@@ -150,7 +150,7 @@ module HTTPX
|
|
|
150
150
|
request.headers.add("if-modified-since", last_modified)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"])
|
|
153
|
+
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"])
|
|
154
154
|
request.headers.add("if-none-match", etag)
|
|
155
155
|
end
|
|
156
156
|
end
|
|
@@ -296,9 +296,7 @@ module HTTPX
|
|
|
296
296
|
return @cache_control if defined?(@cache_control)
|
|
297
297
|
|
|
298
298
|
@cache_control = begin
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
@headers["cache-control"].split(/ *, */)
|
|
299
|
+
@headers["cache-control"].split(/ *, */) if @headers.key?("cache-control")
|
|
302
300
|
end
|
|
303
301
|
end
|
|
304
302
|
|
|
@@ -307,9 +305,7 @@ module HTTPX
|
|
|
307
305
|
return @vary if defined?(@vary)
|
|
308
306
|
|
|
309
307
|
@vary = begin
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@headers["vary"].split(/ *, */).map(&:downcase)
|
|
308
|
+
@headers["vary"].split(/ *, */).map(&:downcase) if @headers.key?("vary")
|
|
313
309
|
end
|
|
314
310
|
end
|
|
315
311
|
|
|
@@ -36,15 +36,33 @@ module HTTPX
|
|
|
36
36
|
Parser::Error,
|
|
37
37
|
TimeoutError,
|
|
38
38
|
]).freeze
|
|
39
|
-
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }.freeze
|
|
41
|
+
|
|
42
|
+
# list of supported backoff algorithms
|
|
43
|
+
BACKOFF_ALGORITHMS = %i[exponential_backoff polynomial_backoff].freeze
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
if ENV.key?("HTTPX_NO_JITTER")
|
|
47
|
+
def extra_options(options)
|
|
48
|
+
options.merge(max_retries: MAX_RETRIES)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
def extra_options(options)
|
|
52
|
+
options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
|
|
53
|
+
end
|
|
44
54
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
|
|
56
|
+
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy.
|
|
57
|
+
def retry_after_polynomial_backoff(request, _)
|
|
58
|
+
offset = request.options.max_retries - request.retries
|
|
59
|
+
2 * (offset - 1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy.
|
|
63
|
+
def retry_after_exponential_backoff(request, _)
|
|
64
|
+
offset = request.options.max_retries - request.retries
|
|
65
|
+
(offset - 1) * 2
|
|
48
66
|
end
|
|
49
67
|
end
|
|
50
68
|
|
|
@@ -53,6 +71,7 @@ module HTTPX
|
|
|
53
71
|
# :max_retries :: max number of times a request will be retried (defaults to <tt>3</tt>).
|
|
54
72
|
# :retry_change_requests :: whether idempotent requests are retried (defaults to <tt>false</tt>).
|
|
55
73
|
# :retry_after:: seconds after which a request is retried; can also be a callable object (i.e. <tt>->(req, res) { ... } </tt>)
|
|
74
|
+
# or the name of a supported backoff algorithm (i.e. <tt>:exponential_backoff</tt>).
|
|
56
75
|
# :retry_jitter :: number of seconds applied to *:retry_after* (must be a callable, i.e. <tt>->(retry_after) { ... } </tt>).
|
|
57
76
|
# :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
|
|
58
77
|
# (i.e. <tt>->(res) { ... }</tt>).
|
|
@@ -60,10 +79,26 @@ module HTTPX
|
|
|
60
79
|
private
|
|
61
80
|
|
|
62
81
|
def option_retry_after(value)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
if value.respond_to?(:call)
|
|
83
|
+
value1 = value
|
|
84
|
+
value1 = value1.method(:call) unless value1.respond_to?(:arity)
|
|
85
|
+
|
|
86
|
+
# allow ->(*) arity as well, which is < 0
|
|
87
|
+
raise TypeError, "`:retry_after` proc has invalid number of parameters" unless value1.arity.negative? || value1.arity.between?(
|
|
88
|
+
1, 2
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
else
|
|
92
|
+
case value
|
|
93
|
+
when Symbol
|
|
94
|
+
raise TypeError, "`retry_after`: `#{value}` is not a supported backoff algorithm" unless BACKOFF_ALGORITHMS.include?(value)
|
|
95
|
+
|
|
96
|
+
value = Retries.method(:"retry_after_#{value}")
|
|
97
|
+
|
|
98
|
+
else
|
|
99
|
+
value = Float(value)
|
|
100
|
+
raise TypeError, "`:retry_after` must be positive" unless value.positive?
|
|
101
|
+
end
|
|
67
102
|
end
|
|
68
103
|
|
|
69
104
|
value
|
|
@@ -107,19 +142,11 @@ module HTTPX
|
|
|
107
142
|
|
|
108
143
|
if response &&
|
|
109
144
|
request.retries.positive? &&
|
|
110
|
-
|
|
111
|
-
(
|
|
112
|
-
(
|
|
113
|
-
response.is_a?(ErrorResponse) && retryable_error?(response.error)
|
|
114
|
-
) ||
|
|
115
|
-
(
|
|
116
|
-
options.retry_on && options.retry_on.call(response)
|
|
117
|
-
)
|
|
118
|
-
)
|
|
145
|
+
retryable_request?(request, response, options) &&
|
|
146
|
+
retryable_response?(response, options)
|
|
119
147
|
try_partial_retry(request, response)
|
|
120
148
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
121
|
-
request
|
|
122
|
-
request.transition(:idle)
|
|
149
|
+
prepare_to_retry(request, response)
|
|
123
150
|
|
|
124
151
|
retry_after = options.retry_after
|
|
125
152
|
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
@@ -152,12 +179,16 @@ module HTTPX
|
|
|
152
179
|
end
|
|
153
180
|
|
|
154
181
|
# returns whether +request+ can be retried.
|
|
155
|
-
def
|
|
182
|
+
def retryable_request?(request, _, options)
|
|
156
183
|
IDEMPOTENT_METHODS.include?(request.verb) || options.retry_change_requests
|
|
157
184
|
end
|
|
158
185
|
|
|
186
|
+
def retryable_response?(response, options)
|
|
187
|
+
(response.is_a?(ErrorResponse) && retryable_error?(response.error, options)) || options.retry_on&.call(response)
|
|
188
|
+
end
|
|
189
|
+
|
|
159
190
|
# returns whether the +ex+ exception happend for a retriable request.
|
|
160
|
-
def retryable_error?(ex)
|
|
191
|
+
def retryable_error?(ex, _)
|
|
161
192
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
|
162
193
|
end
|
|
163
194
|
|
|
@@ -165,6 +196,11 @@ module HTTPX
|
|
|
165
196
|
super && !request.retries.positive?
|
|
166
197
|
end
|
|
167
198
|
|
|
199
|
+
def prepare_to_retry(request, _response)
|
|
200
|
+
request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
|
|
201
|
+
request.transition(:idle)
|
|
202
|
+
end
|
|
203
|
+
|
|
168
204
|
#
|
|
169
205
|
# Attempt to set the request to perform a partial range request.
|
|
170
206
|
# This happens if the peer server accepts byte-range requests, and
|