httpx 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/altsvc.rb +3 -1
- data/lib/httpx/connection/http1.rb +5 -6
- data/lib/httpx/connection/http2.rb +2 -0
- data/lib/httpx/connection.rb +3 -8
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/loggable.rb +2 -0
- data/lib/httpx/options.rb +40 -17
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +87 -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 +162 -56
- data/lib/httpx/plugins/rate_limiter.rb +2 -2
- data/lib/httpx/plugins/response_cache.rb +3 -7
- data/lib/httpx/plugins/retries.rb +55 -16
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +59 -8
- data/lib/httpx/plugins/stream_bidi.rb +73 -17
- data/lib/httpx/pool.rb +12 -2
- data/lib/httpx/request.rb +10 -1
- data/lib/httpx/resolver/https.rb +67 -17
- data/lib/httpx/resolver/multi.rb +4 -0
- data/lib/httpx/resolver/native.rb +26 -4
- data/lib/httpx/resolver/resolver.rb +2 -2
- data/lib/httpx/resolver.rb +97 -29
- data/lib/httpx/response/body.rb +2 -0
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +9 -0
- data/lib/httpx/session.rb +6 -6
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +16 -8
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +3 -0
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +1 -3
- data/sig/options.rbs +1 -1
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +28 -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 +46 -15
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +8 -2
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/resolver/https.rbs +5 -0
- data/sig/resolver/multi.rbs +2 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver.rbs +12 -3
- data/sig/response.rbs +3 -0
- data/sig/session.rbs +3 -5
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- metadata +3 -1
|
@@ -6,12 +6,13 @@ module HTTPX
|
|
|
6
6
|
#
|
|
7
7
|
# This enables integration with fiber scheduler implementations such as [async](https://github.com/async).
|
|
8
8
|
#
|
|
9
|
-
# # https://gitlab.com/os85/httpx/wikis/
|
|
9
|
+
# # https://gitlab.com/os85/httpx/wikis/Fiber-Concurrency
|
|
10
10
|
#
|
|
11
11
|
module FiberConcurrency
|
|
12
12
|
def self.subplugins
|
|
13
13
|
{
|
|
14
14
|
h2c: FiberConcurrencyH2C,
|
|
15
|
+
stream: FiberConcurrencyStream,
|
|
15
16
|
}
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -188,6 +189,20 @@ module HTTPX
|
|
|
188
189
|
end
|
|
189
190
|
end
|
|
190
191
|
end
|
|
192
|
+
|
|
193
|
+
module FiberConcurrencyStream
|
|
194
|
+
module StreamResponseMethods
|
|
195
|
+
def close
|
|
196
|
+
unless @request.current_context?
|
|
197
|
+
@request.close
|
|
198
|
+
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
super
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
191
206
|
end
|
|
192
207
|
|
|
193
208
|
register_plugin :fiber_concurrency, FiberConcurrency
|
|
@@ -48,7 +48,7 @@ module HTTPX
|
|
|
48
48
|
until message.empty?
|
|
49
49
|
compressed, size = message.unpack("CL>")
|
|
50
50
|
|
|
51
|
-
encoded_data = message.byteslice(5..size + 5 - 1)
|
|
51
|
+
encoded_data = message.byteslice(5..(size + 5 - 1))
|
|
52
52
|
|
|
53
53
|
if compressed == 1
|
|
54
54
|
grpc_encodings.reverse_each do |encoding|
|
data/lib/httpx/plugins/grpc.rb
CHANGED
|
@@ -249,7 +249,7 @@ module HTTPX
|
|
|
249
249
|
call
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
-
def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **)
|
|
252
|
+
def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **opts)
|
|
253
253
|
uri = @options.origin.dup
|
|
254
254
|
rpc_method = "/#{rpc_method}" unless rpc_method.start_with?("/")
|
|
255
255
|
rpc_method = "/#{@options.grpc_service}#{rpc_method}" if @options.grpc_service
|
|
@@ -273,7 +273,7 @@ module HTTPX
|
|
|
273
273
|
|
|
274
274
|
headers.merge!(@options.call_credentials.call.transform_keys(&:to_s)) if @options.call_credentials
|
|
275
275
|
|
|
276
|
-
build_request("POST", uri, headers: headers, body: input)
|
|
276
|
+
build_request("POST", uri, headers: headers, body: input, **opts)
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
end
|
|
@@ -7,8 +7,9 @@ module HTTPX
|
|
|
7
7
|
#
|
|
8
8
|
module NTLMAuth
|
|
9
9
|
class << self
|
|
10
|
-
def load_dependencies(
|
|
10
|
+
def load_dependencies(klass)
|
|
11
11
|
require_relative "auth/ntlm"
|
|
12
|
+
klass.plugin(:auth)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def extra_options(options)
|
|
@@ -38,14 +39,15 @@ module HTTPX
|
|
|
38
39
|
ntlm = request.options.ntlm
|
|
39
40
|
|
|
40
41
|
if ntlm
|
|
41
|
-
request.
|
|
42
|
+
request.authorize(ntlm.negotiate)
|
|
42
43
|
probe_response = wrap { super(request).first }
|
|
43
44
|
|
|
44
45
|
return probe_response unless probe_response.is_a?(Response)
|
|
45
46
|
|
|
46
47
|
if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
|
|
47
48
|
request.transition(:idle)
|
|
48
|
-
request.headers
|
|
49
|
+
request.headers.get("authorization").pop
|
|
50
|
+
request.authorize(ntlm.authenticate(request, probe_response.headers["www-authenticate"]).encode("utf-8"))
|
|
49
51
|
super(request)
|
|
50
52
|
else
|
|
51
53
|
probe_response
|
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,32 @@ 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
|
|
|
102
183
|
module OptionsMethods
|
|
103
184
|
private
|
|
104
185
|
|
|
105
186
|
def option_oauth_session(value)
|
|
187
|
+
warn "DEPRECATION WARNING: option `:oauth_session` is deprecated. " \
|
|
188
|
+
"Use `:oauth_options` instead."
|
|
189
|
+
|
|
106
190
|
case value
|
|
107
191
|
when Hash
|
|
108
192
|
OAuthSession.new(**value)
|
|
@@ -112,69 +196,91 @@ module HTTPX
|
|
|
112
196
|
raise TypeError, ":oauth_session must be a #{OAuthSession}"
|
|
113
197
|
end
|
|
114
198
|
end
|
|
199
|
+
|
|
200
|
+
def option_oauth_options(value)
|
|
201
|
+
value = Hash[value] unless value.is_a?(Hash)
|
|
202
|
+
value
|
|
203
|
+
end
|
|
115
204
|
end
|
|
116
205
|
|
|
117
206
|
module InstanceMethods
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
207
|
+
attr_reader :oauth_session
|
|
208
|
+
protected :oauth_session
|
|
121
209
|
|
|
122
|
-
def
|
|
123
|
-
|
|
210
|
+
def initialize(*)
|
|
211
|
+
super
|
|
124
212
|
|
|
125
|
-
oauth_session.
|
|
213
|
+
@oauth_session = if @options.oauth_options
|
|
214
|
+
OAuthSession.new(**@options.oauth_options)
|
|
215
|
+
elsif @options.oauth_session
|
|
216
|
+
@oauth_session = @options.oauth_session.dup
|
|
217
|
+
end
|
|
218
|
+
end
|
|
126
219
|
|
|
127
|
-
|
|
220
|
+
def initialize_dup(other)
|
|
221
|
+
super
|
|
222
|
+
@oauth_session = other.instance_variable_get(:@oauth_session).dup
|
|
223
|
+
end
|
|
128
224
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"scope" => Array(oauth_session.scope).join(" "),
|
|
133
|
-
"audience" => oauth_session.audience,
|
|
134
|
-
}.compact
|
|
225
|
+
def oauth_auth(**args)
|
|
226
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
227
|
+
"Use `with(oauth_options: options)` instead."
|
|
135
228
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
when "client_secret_post"
|
|
139
|
-
form_post["client_id"] = oauth_session.client_id
|
|
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
|
|
143
|
-
end
|
|
229
|
+
with(oauth_options: args)
|
|
230
|
+
end
|
|
144
231
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
232
|
+
# will eagerly negotiate new oauth tokens with the issuer
|
|
233
|
+
def refresh_oauth_tokens!
|
|
234
|
+
return unless @oauth_session
|
|
235
|
+
|
|
236
|
+
@oauth_session.reset!
|
|
237
|
+
@oauth_session.fetch_access_token(self)
|
|
238
|
+
end
|
|
151
239
|
|
|
152
|
-
|
|
153
|
-
|
|
240
|
+
# TODO: deprecate
|
|
241
|
+
def with_access_token
|
|
242
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
243
|
+
"The session will automatically handle token lifecycles for you."
|
|
154
244
|
|
|
155
|
-
|
|
156
|
-
|
|
245
|
+
other_session = dup # : instance
|
|
246
|
+
oauth_session = other_session.oauth_session
|
|
247
|
+
oauth_session.fetch_access_token(other_session)
|
|
248
|
+
other_session
|
|
249
|
+
end
|
|
157
250
|
|
|
158
|
-
|
|
251
|
+
private
|
|
159
252
|
|
|
160
|
-
|
|
161
|
-
|
|
253
|
+
def generate_auth_token
|
|
254
|
+
return unless @oauth_session
|
|
162
255
|
|
|
163
|
-
|
|
256
|
+
@oauth_session.fetch_access_token(self)
|
|
164
257
|
end
|
|
258
|
+
end
|
|
165
259
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
260
|
+
module OAuthRetries
|
|
261
|
+
class << self
|
|
262
|
+
def extra_options(options)
|
|
263
|
+
options.merge(
|
|
264
|
+
retry_on: method(:response_oauth_error?),
|
|
265
|
+
generate_auth_value_on_retry: method(:response_oauth_error?)
|
|
266
|
+
)
|
|
267
|
+
end
|
|
170
268
|
|
|
171
|
-
|
|
269
|
+
def response_oauth_error?(res)
|
|
270
|
+
res.is_a?(Response) && res.status == 401
|
|
271
|
+
end
|
|
272
|
+
end
|
|
172
273
|
|
|
173
|
-
|
|
274
|
+
module InstanceMethods
|
|
275
|
+
def prepare_to_retry(_request, response)
|
|
276
|
+
unless @oauth_session && @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
|
|
277
|
+
return super
|
|
278
|
+
end
|
|
174
279
|
|
|
175
|
-
|
|
280
|
+
@oauth_session.reset!
|
|
176
281
|
|
|
177
|
-
|
|
282
|
+
super
|
|
283
|
+
end
|
|
178
284
|
end
|
|
179
285
|
end
|
|
180
286
|
end
|
|
@@ -18,11 +18,11 @@ module HTTPX
|
|
|
18
18
|
def configure(klass)
|
|
19
19
|
klass.plugin(:retries,
|
|
20
20
|
retry_change_requests: true,
|
|
21
|
-
retry_on: method(:retry_on_rate_limited_response),
|
|
21
|
+
retry_on: method(:retry_on_rate_limited_response?),
|
|
22
22
|
retry_after: method(:retry_after_rate_limit))
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def retry_on_rate_limited_response(response)
|
|
25
|
+
def retry_on_rate_limited_response?(response)
|
|
26
26
|
return false unless response.is_a?(Response)
|
|
27
27
|
|
|
28
28
|
status = response.status
|
|
@@ -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
|
|
54
|
+
end
|
|
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)
|
|
44
60
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
@@ -112,14 +147,13 @@ module HTTPX
|
|
|
112
147
|
(
|
|
113
148
|
response.is_a?(ErrorResponse) && retryable_error?(response.error)
|
|
114
149
|
) ||
|
|
115
|
-
|
|
116
|
-
options.retry_on
|
|
117
|
-
|
|
150
|
+
|
|
151
|
+
options.retry_on&.call(response)
|
|
152
|
+
|
|
118
153
|
)
|
|
119
154
|
try_partial_retry(request, response)
|
|
120
155
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
121
|
-
request
|
|
122
|
-
request.transition(:idle)
|
|
156
|
+
prepare_to_retry(request, response)
|
|
123
157
|
|
|
124
158
|
retry_after = options.retry_after
|
|
125
159
|
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
@@ -165,6 +199,11 @@ module HTTPX
|
|
|
165
199
|
super && !request.retries.positive?
|
|
166
200
|
end
|
|
167
201
|
|
|
202
|
+
def prepare_to_retry(request, _response)
|
|
203
|
+
request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
|
|
204
|
+
request.transition(:idle)
|
|
205
|
+
end
|
|
206
|
+
|
|
168
207
|
#
|
|
169
208
|
# Attempt to set the request to perform a partial range request.
|
|
170
209
|
# This happens if the peer server accepts byte-range requests, and
|
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -55,9 +55,9 @@ module HTTPX
|
|
|
55
55
|
line << chunk
|
|
56
56
|
|
|
57
57
|
while (idx = line.index("\n"))
|
|
58
|
-
yield line.byteslice(0..idx - 1)
|
|
58
|
+
yield line.byteslice(0..(idx - 1))
|
|
59
59
|
|
|
60
|
-
line = line.byteslice(idx + 1..-1)
|
|
60
|
+
line = line.byteslice((idx + 1)..-1)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -121,20 +121,71 @@ module HTTPX
|
|
|
121
121
|
# https://gitlab.com/os85/httpx/wikis/Stream
|
|
122
122
|
#
|
|
123
123
|
module Stream
|
|
124
|
+
STREAM_REQUEST_OPTIONS = { timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 }.freeze }.freeze
|
|
125
|
+
|
|
124
126
|
def self.extra_options(options)
|
|
125
|
-
options.merge(
|
|
127
|
+
options.merge(
|
|
128
|
+
stream: false,
|
|
129
|
+
timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 },
|
|
130
|
+
stream_response_class: Class.new(StreamResponse, &Options::SET_TEMPORARY_NAME).freeze
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# adds support for the following options:
|
|
135
|
+
#
|
|
136
|
+
# :stream :: whether the request to process should be handled as a stream (defaults to <tt>false</tt>).
|
|
137
|
+
# :stream_response_class :: Class used to build the stream response object.
|
|
138
|
+
module OptionsMethods
|
|
139
|
+
def option_stream(val)
|
|
140
|
+
val
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def option_stream_response_class(value)
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def extend_with_plugin_classes(pl)
|
|
148
|
+
return super unless defined?(pl::StreamResponseMethods)
|
|
149
|
+
|
|
150
|
+
@stream_response_class = @stream_response_class.dup
|
|
151
|
+
Options::SET_TEMPORARY_NAME[@stream_response_class, pl]
|
|
152
|
+
@stream_response_class.__send__(:include, pl::StreamResponseMethods) if defined?(pl::StreamResponseMethods)
|
|
153
|
+
|
|
154
|
+
super
|
|
155
|
+
end
|
|
126
156
|
end
|
|
127
157
|
|
|
128
158
|
module InstanceMethods
|
|
129
|
-
def request(*args,
|
|
130
|
-
|
|
159
|
+
def request(*args, **options)
|
|
160
|
+
if args.first.is_a?(Request)
|
|
161
|
+
requests = args
|
|
162
|
+
|
|
163
|
+
request = requests.first
|
|
164
|
+
|
|
165
|
+
unless request.options.stream && !request.stream
|
|
166
|
+
if options[:stream]
|
|
167
|
+
warn "passing `stream: true` with a request obkect is not supported anymore. " \
|
|
168
|
+
"You can instead build the request object with `stream :true`"
|
|
169
|
+
end
|
|
170
|
+
return super
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
return super unless options[:stream]
|
|
174
|
+
|
|
175
|
+
requests = build_requests(*args, options)
|
|
176
|
+
|
|
177
|
+
request = requests.first
|
|
178
|
+
end
|
|
131
179
|
|
|
132
|
-
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
|
133
180
|
raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
|
|
134
181
|
|
|
135
|
-
request
|
|
182
|
+
@options.stream_response_class.new(request, self)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_request(verb, uri, params = EMPTY_HASH, options = @options)
|
|
186
|
+
return super unless params[:stream]
|
|
136
187
|
|
|
137
|
-
|
|
188
|
+
super(verb, uri, params, options.merge(STREAM_REQUEST_OPTIONS.merge(stream: true)))
|
|
138
189
|
end
|
|
139
190
|
end
|
|
140
191
|
|