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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +2 -2
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/altsvc.rb +3 -1
  8. data/lib/httpx/connection/http1.rb +5 -6
  9. data/lib/httpx/connection/http2.rb +2 -0
  10. data/lib/httpx/connection.rb +3 -8
  11. data/lib/httpx/domain_name.rb +1 -1
  12. data/lib/httpx/headers.rb +2 -2
  13. data/lib/httpx/loggable.rb +2 -0
  14. data/lib/httpx/options.rb +40 -17
  15. data/lib/httpx/plugins/auth/digest.rb +44 -4
  16. data/lib/httpx/plugins/auth.rb +87 -4
  17. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  18. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  19. data/lib/httpx/plugins/digest_auth.rb +4 -5
  20. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  21. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  22. data/lib/httpx/plugins/grpc.rb +2 -2
  23. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  24. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  25. data/lib/httpx/plugins/oauth.rb +162 -56
  26. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  27. data/lib/httpx/plugins/response_cache.rb +3 -7
  28. data/lib/httpx/plugins/retries.rb +55 -16
  29. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  30. data/lib/httpx/plugins/stream.rb +59 -8
  31. data/lib/httpx/plugins/stream_bidi.rb +73 -17
  32. data/lib/httpx/pool.rb +12 -2
  33. data/lib/httpx/request.rb +10 -1
  34. data/lib/httpx/resolver/https.rb +67 -17
  35. data/lib/httpx/resolver/multi.rb +4 -0
  36. data/lib/httpx/resolver/native.rb +26 -4
  37. data/lib/httpx/resolver/resolver.rb +2 -2
  38. data/lib/httpx/resolver.rb +97 -29
  39. data/lib/httpx/response/body.rb +2 -0
  40. data/lib/httpx/response.rb +22 -6
  41. data/lib/httpx/selector.rb +9 -0
  42. data/lib/httpx/session.rb +6 -6
  43. data/lib/httpx/transcoder/body.rb +1 -1
  44. data/lib/httpx/transcoder/json.rb +1 -1
  45. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  46. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  47. data/lib/httpx/transcoder/multipart.rb +16 -8
  48. data/lib/httpx/transcoder.rb +4 -6
  49. data/lib/httpx/version.rb +1 -1
  50. data/sig/altsvc.rbs +3 -0
  51. data/sig/chainable.rbs +3 -3
  52. data/sig/connection.rbs +1 -3
  53. data/sig/options.rbs +1 -1
  54. data/sig/plugins/auth/digest.rbs +6 -0
  55. data/sig/plugins/auth.rbs +28 -4
  56. data/sig/plugins/basic_auth.rbs +3 -3
  57. data/sig/plugins/digest_auth.rbs +2 -4
  58. data/sig/plugins/fiber_concurrency.rbs +6 -0
  59. data/sig/plugins/ntlm_auth.rbs +2 -2
  60. data/sig/plugins/oauth.rbs +46 -15
  61. data/sig/plugins/rate_limiter.rbs +1 -1
  62. data/sig/plugins/response_cache/file_store.rbs +2 -0
  63. data/sig/plugins/response_cache.rbs +4 -0
  64. data/sig/plugins/retries.rbs +8 -2
  65. data/sig/plugins/stream.rbs +13 -3
  66. data/sig/plugins/stream_bidi.rbs +2 -2
  67. data/sig/pool.rbs +1 -1
  68. data/sig/resolver/https.rbs +5 -0
  69. data/sig/resolver/multi.rbs +2 -0
  70. data/sig/resolver/native.rbs +2 -0
  71. data/sig/resolver.rbs +12 -3
  72. data/sig/response.rbs +3 -0
  73. data/sig/session.rbs +3 -5
  74. data/sig/transcoder/multipart.rbs +4 -2
  75. data/sig/transcoder.rbs +5 -1
  76. 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/FiberConcurrency
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|
@@ -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
@@ -45,7 +45,7 @@ module HTTPX
45
45
  debug_level: @options ? @options.debug_level : DEBUG_LEVEL,
46
46
  debug: nil
47
47
  ) do
48
- "[ELAPSED TIME]: #{label}: #{elapsed} (ms)" << "\e[0m"
48
+ "[ELAPSED TIME]: #{label}: #{elapsed} (ms)\e[0m"
49
49
  end
50
50
  end
51
51
  end
@@ -7,8 +7,9 @@ module HTTPX
7
7
  #
8
8
  module NTLMAuth
9
9
  class << self
10
- def load_dependencies(_klass)
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.headers["authorization"] = ntlm.negotiate
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["authorization"] = ntlm.authenticate(request, probe_response.headers["www-authenticate"])
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
@@ -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(_klass)
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 :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope, :audience
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 load(http)
69
- return if @grant_type && @scope
87
+ def reset!
88
+ @access_token = nil
89
+ end
70
90
 
71
- metadata = http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json
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
- @token_endpoint = metadata["token_endpoint"]
74
- @scope = metadata["scopes_supported"]
75
- @grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
76
- @token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
77
- SUPPORTED_AUTH_METHODS.include?(am)
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
- nil
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
- def oauth_auth(**args)
119
- with(oauth_session: OAuthSession.new(**args))
120
- end
207
+ attr_reader :oauth_session
208
+ protected :oauth_session
121
209
 
122
- def with_access_token
123
- oauth_session = @options.oauth_session
210
+ def initialize(*)
211
+ super
124
212
 
125
- oauth_session.load(self)
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
- grant_type = oauth_session.grant_type
220
+ def initialize_dup(other)
221
+ super
222
+ @oauth_session = other.instance_variable_get(:@oauth_session).dup
223
+ end
128
224
 
129
- headers = {}
130
- form_post = {
131
- "grant_type" => grant_type,
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
- # auth
137
- case oauth_session.token_endpoint_auth_method
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
- case grant_type
146
- when "client_credentials"
147
- # do nothing
148
- when "refresh_token"
149
- form_post["refresh_token"] = oauth_session.refresh_token
150
- end
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
- token_request = build_request("POST", oauth_session.token_endpoint, headers: headers, form: form_post)
153
- token_request.headers.delete("authorization") unless oauth_session.token_endpoint_auth_method == "client_secret_basic"
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
- token_response = request(token_request)
156
- token_response.raise_for_status
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
- payload = token_response.json
251
+ private
159
252
 
160
- access_token = payload["access_token"]
161
- refresh_token = payload["refresh_token"]
253
+ def generate_auth_token
254
+ return unless @oauth_session
162
255
 
163
- with(oauth_session: oauth_session.merge(access_token: access_token, refresh_token: refresh_token))
256
+ @oauth_session.fetch_access_token(self)
164
257
  end
258
+ end
165
259
 
166
- def build_request(*)
167
- request = super
168
-
169
- return request if request.headers.key?("authorization")
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
- oauth_session = @options.oauth_session
269
+ def response_oauth_error?(res)
270
+ res.is_a?(Response) && res.status == 401
271
+ end
272
+ end
172
273
 
173
- return request unless oauth_session && oauth_session.access_token
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
- request.headers["authorization"] = "Bearer #{oauth_session.access_token}"
280
+ @oauth_session.reset!
176
281
 
177
- request
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"]) # rubocop:disable Style/GuardClause
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
- return unless @headers.key?("cache-control")
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
- return unless @headers.key?("vary")
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
- if ENV.key?("HTTPX_NO_JITTER")
42
- def self.extra_options(options)
43
- options.merge(max_retries: MAX_RETRIES)
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
- else
46
- def self.extra_options(options)
47
- options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
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
- # return early if callable
64
- unless value.respond_to?(:call)
65
- value = Float(value)
66
- raise TypeError, ":retry_after must be positive" unless value.positive?
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 && options.retry_on.call(response)
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.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
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
@@ -16,7 +16,7 @@ module HTTPX
16
16
  mask_addr = @mask_addr
17
17
  raise "Invalid mask" if mask_addr.zero?
18
18
 
19
- mask_addr >>= 1 while (mask_addr & 0x1).zero?
19
+ mask_addr >>= 1 while mask_addr.nobits?(0x1)
20
20
 
21
21
  length = 0
22
22
  while mask_addr & 0x1 == 0x1
@@ -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(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
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, stream: false, **options)
130
- return super(*args, **options) unless stream
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 = requests.first
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
- StreamResponse.new(request, self)
188
+ super(verb, uri, params, options.merge(STREAM_REQUEST_OPTIONS.merge(stream: true)))
138
189
  end
139
190
  end
140
191