httpx 1.7.0 → 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +21 -0
  3. data/lib/httpx/adapters/webmock.rb +18 -9
  4. data/lib/httpx/altsvc.rb +1 -1
  5. data/lib/httpx/connection/http1.rb +4 -3
  6. data/lib/httpx/connection.rb +4 -1
  7. data/lib/httpx/io/tcp.rb +1 -1
  8. data/lib/httpx/options.rb +78 -5
  9. data/lib/httpx/parser/http1.rb +1 -0
  10. data/lib/httpx/plugins/auth.rb +28 -2
  11. data/lib/httpx/plugins/oauth.rb +11 -18
  12. data/lib/httpx/plugins/persistent.rb +3 -5
  13. data/lib/httpx/plugins/proxy/http.rb +0 -4
  14. data/lib/httpx/plugins/proxy.rb +3 -1
  15. data/lib/httpx/plugins/query.rb +1 -1
  16. data/lib/httpx/plugins/rate_limiter.rb +20 -15
  17. data/lib/httpx/plugins/retries.rb +8 -11
  18. data/lib/httpx/plugins/stream.rb +2 -2
  19. data/lib/httpx/plugins/stream_bidi.rb +13 -1
  20. data/lib/httpx/pool.rb +0 -1
  21. data/lib/httpx/request/body.rb +1 -1
  22. data/lib/httpx/resolver/cache/base.rb +136 -0
  23. data/lib/httpx/resolver/cache/memory.rb +42 -0
  24. data/lib/httpx/resolver/cache.rb +18 -0
  25. data/lib/httpx/resolver/https.rb +7 -3
  26. data/lib/httpx/resolver/multi.rb +7 -3
  27. data/lib/httpx/resolver/native.rb +6 -2
  28. data/lib/httpx/resolver/resolver.rb +1 -1
  29. data/lib/httpx/resolver.rb +3 -149
  30. data/lib/httpx/response/body.rb +3 -3
  31. data/lib/httpx/selector.rb +5 -3
  32. data/lib/httpx/session.rb +1 -1
  33. data/lib/httpx/timers.rb +6 -12
  34. data/lib/httpx/transcoder/gzip.rb +7 -2
  35. data/lib/httpx/transcoder/multipart/decoder.rb +1 -1
  36. data/lib/httpx/transcoder/multipart.rb +1 -1
  37. data/lib/httpx/utils.rb +13 -0
  38. data/lib/httpx/version.rb +1 -1
  39. data/sig/altsvc.rbs +7 -4
  40. data/sig/loggable.rbs +1 -1
  41. data/sig/options.rbs +11 -3
  42. data/sig/plugins/auth.rbs +10 -1
  43. data/sig/plugins/oauth.rbs +0 -2
  44. data/sig/plugins/rate_limiter.rbs +4 -2
  45. data/sig/plugins/retries.rbs +4 -2
  46. data/sig/resolver/cache/base.rbs +28 -0
  47. data/sig/resolver/cache/memory.rbs +13 -0
  48. data/sig/resolver/cache.rbs +16 -0
  49. data/sig/resolver/https.rbs +19 -0
  50. data/sig/resolver/multi.rbs +6 -0
  51. data/sig/resolver.rbs +0 -24
  52. data/sig/timers.rbs +1 -1
  53. data/sig/utils.rbs +2 -0
  54. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2af63a63fe08211db58764570618b2e7c234d3473a28fc1ad6f5a31c3d0fb13d
4
- data.tar.gz: 6b2671b85ac69e4817b8b764dfdabf638dacf94cfdcab6c44627ad64a57bcb50
3
+ metadata.gz: a9c30e22a2d406a61ef87a58fddd607bb2b1fad7b50d837d8db062d4a538a2d2
4
+ data.tar.gz: 9c5e1997b9c03434071c59b2deb2b004f7b1a1077c2ccaed03b9e3a1db7aafe5
5
5
  SHA512:
6
- metadata.gz: 50a2d2d3c0cb27f3bf84cc34553b06bd9fadf46ad789f9cfa08091cc458ac07751cb5593e01d242d88e444f10ac2b3014ea3e4e56f7c616ebc860517d08ef90d
7
- data.tar.gz: 6b15f21262e85639c6d32b9851926d7e717b6f9bcbc226b9af3872d67461799fd24847a2d563dfbad1967d5349d371c18a1a6e1a28c721c5f33387f2fc1446a7
6
+ metadata.gz: 87d99c4971b99f086f7811be81a8453e71bd2535120b5e560a2fc837d50e8a040e70ab1b2b191beee4b481fcea5063576d8895d318a0089183c21cb59fdd0f24
7
+ data.tar.gz: 85469cf5b5822990367f1e5591798a1512de90a60226853da4b4e6b00dde8816d6218dd2aacc9b5d85f84f6cb1637dd4f7b37836863f44144f05329e4fbdf7f5
@@ -0,0 +1,21 @@
1
+ # 1.7.1
2
+
3
+ ## Improvements
4
+
5
+ * fixed timers handling in the selector loop which caused them to be traversed-for-drop twice on each tick.
6
+ * connection: take proxy connecting states when transitioning to `:closing` state.
7
+ * refactored name resolution cache to a cache adapter API with a default memory cache which keeps the current behaviour and will allow to add others.
8
+ * a new option, `:resolver_cache`, was added, which is `:memory` by default.
9
+ * this fixes an issue introduced for multi-ractor support where the cache store would not be thread-safe when used in a non main ractor.
10
+ * `:auth` plugin: when loaded with the `:retries` plugin, and the auth value method is dynamic/callable, will recover out-of-the-box from 401 HTTP responses by retrying the request with a newly-generated token.
11
+ * the `:rate_limiter` plugin will use this work to retry rate-limited responses without having to set setting `:retry_change_requests` to true, thereby eliminating a potential issue with non-idempotent requests.
12
+
13
+ ## Bugfixes
14
+
15
+ * HTTP1 parser: clear buffer on reset.
16
+ * http1 fix: handle the case in `#handle_error` where the response is an error response
17
+ * `:stream_bidi` plugin: fix internal state preventing bidi requests from being retried.
18
+ * `:stream_bidi` plugin: will only allow initial request body being passed using `:body` param (others, like `:json`, will raise an exception)
19
+ * https resolver: return `:idle` on `#state` calls when no connection is available (sometimes called in internal log messages).
20
+ * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
21
+ *
@@ -38,12 +38,15 @@ module WebMock
38
38
 
39
39
  return build_error_response(request, webmock_response.exception) if webmock_response.exception
40
40
 
41
- request.options.response_class.new(request,
42
- webmock_response.status[0],
43
- "2.0",
44
- webmock_response.headers).tap do |res|
45
- res.mocked = true
46
- end
41
+ request
42
+ .options
43
+ .response_class
44
+ .new(
45
+ request,
46
+ webmock_response.status[0],
47
+ "2.0",
48
+ webmock_response.headers
49
+ ).tap(&:mock!)
47
50
  end
48
51
 
49
52
  def build_error_response(request, exception)
@@ -72,17 +75,23 @@ module WebMock
72
75
  end
73
76
 
74
77
  module ResponseMethods
75
- attr_accessor :mocked
76
-
77
78
  def initialize(*)
78
79
  super
79
80
  @mocked = false
80
81
  end
82
+
83
+ def mock!
84
+ @mocked = true
85
+ end
86
+
87
+ def mocked?
88
+ @mocked
89
+ end
81
90
  end
82
91
 
83
92
  module ResponseBodyMethods
84
93
  def decode_chunk(chunk)
85
- return chunk if @response.mocked
94
+ return chunk if @response.mocked?
86
95
 
87
96
  super
88
97
  end
data/lib/httpx/altsvc.rb CHANGED
@@ -43,7 +43,7 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def altsvc_match?(uri, other_uri)
46
- other_uri = URI(other_uri)
46
+ other_uri = URI(other_uri) #: http_uri
47
47
 
48
48
  uri.origin == other_uri.origin || begin
49
49
  case uri.scheme
@@ -197,9 +197,10 @@ module HTTPX
197
197
  end
198
198
 
199
199
  def handle_error(ex, request = nil)
200
- if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request && @request.response &&
201
- !@request.response.headers.key?("content-length") &&
202
- !@request.response.headers.key?("transfer-encoding")
200
+ if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request &&
201
+ (response = @request.response) && response.is_a?(Response) &&
202
+ !response.headers.key?("content-length") &&
203
+ !response.headers.key?("transfer-encoding")
203
204
  # if the response does not contain a content-length header, the server closing the
204
205
  # connnection is the indicator of response consumed.
205
206
  # https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.4
@@ -724,7 +724,7 @@ module HTTPX
724
724
 
725
725
  disconnect
726
726
  when :closing
727
- return unless @state == :idle || @state == :open
727
+ return unless connecting? || @state == :open
728
728
 
729
729
  unless @write_buffer.empty?
730
730
  # preset state before handshake, as error callbacks
@@ -850,6 +850,9 @@ module HTTPX
850
850
  end
851
851
  end
852
852
 
853
+ # recover internal state and emit all relevant error responses when +error+ was raised.
854
+ # this takes an optiona +request+ which may have already been handled and can be opted out
855
+ # in the state recovery process.
853
856
  def handle_error(error, request = nil)
854
857
  parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
855
858
  while (req = @pending.shift)
data/lib/httpx/io/tcp.rb CHANGED
@@ -111,7 +111,7 @@ module HTTPX
111
111
  raise e if @ip_index.negative?
112
112
 
113
113
  log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." }
114
- Resolver.cached_lookup_evict(@hostname, @ip)
114
+ @options.resolver_cache.evict(@hostname, @ip)
115
115
 
116
116
  @io = build_socket
117
117
  retry
data/lib/httpx/options.rb CHANGED
@@ -12,6 +12,7 @@ module HTTPX
12
12
  CLOSE_HANDSHAKE_TIMEOUT = 10
13
13
  CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
14
14
  REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
15
+ RESOLVER_TYPES = %i[memory file].freeze
15
16
 
16
17
  # default value used for "user-agent" header, when not overridden.
17
18
  USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
@@ -44,7 +45,7 @@ module HTTPX
44
45
 
45
46
  return unless meth =~ /^option_(.+)$/
46
47
 
47
- optname = Regexp.last_match(1)
48
+ optname = Regexp.last_match(1) #: String
48
49
 
49
50
  if optname =~ /^(.+[^_])_+with/
50
51
  # ignore alias method chain generated methods.
@@ -52,14 +53,14 @@ module HTTPX
52
53
  # it relies on the "_with/_without" separator, which is the most used convention,
53
54
  # however it shouldn't be used in practice in httpx given the plugin architecture
54
55
  # as the main extension API.
55
- orig_name = Regexp.last_match(1)
56
+ orig_name = Regexp.last_match(1) #: String
56
57
 
57
58
  return if @options_names.include?(orig_name.to_sym)
58
59
  end
59
60
 
60
61
  optname = optname.to_sym
61
62
 
62
- attr_reader(optname)
63
+ attr_reader(optname) unless method_defined?(optname)
63
64
 
64
65
  @options_names << optname unless @options_names.include?(optname)
65
66
  end
@@ -103,7 +104,10 @@ module HTTPX
103
104
  # :io :: open socket, or domain/ip-to-socket hash, which requests should be sent to
104
105
  # :persistent :: whether to persist connections in between requests (defaults to <tt>true</tt>)
105
106
  # :resolver_class :: which resolver to use (defaults to <tt>:native</tt>, can also be <tt>:system<tt> for
106
- # using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class)
107
+ # using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class inheriting
108
+ # from HTTPX::Resolver::Resolver)
109
+ # :resolver_cache :: strategy to cache DNS results, ignored by the <tt>:system</tt> resolver, can be set to <tt>:memory<tt>
110
+ # or an instance of a custom class inheriting from HTTPX::Resolver::Cache::Base
107
111
  # :resolver_options :: hash of options passed to the resolver. Accepted keys depend on the resolver type.
108
112
  # :pool_options :: hash of options passed to the connection pool (See Pool#initialize).
109
113
  # :ip_families :: which socket families are supported (system-dependent)
@@ -151,6 +155,36 @@ module HTTPX
151
155
  freeze
152
156
  end
153
157
 
158
+ # returns the class with which to instantiate the DNS resolver.
159
+ def resolver_class
160
+ case @resolver_class
161
+ when Symbol
162
+ public_send(:"resolver_#{@resolver_class}_class")
163
+ else
164
+ @resolver_class
165
+ end
166
+ end
167
+
168
+ def resolver_cache
169
+ cache_type = @resolver_cache
170
+
171
+ case cache_type
172
+ when :memory
173
+ Resolver::Cache::Memory.cache(cache_type)
174
+ when :file
175
+ Resolver::Cache::File.cache(cache_type)
176
+ else
177
+ unless cache_type.respond_to?(:resolve) &&
178
+ cache_type.respond_to?(:get) &&
179
+ cache_type.respond_to?(:set) &&
180
+ cache_type.respond_to?(:evict)
181
+ raise TypeError, ":resolver_cache must be a compatible resolver cache and implement #get, #set and #evict"
182
+ end
183
+
184
+ cache_type #: Object & Resolver::_Cache
185
+ end
186
+ end
187
+
154
188
  def freeze
155
189
  self.class.options_names.each do |ivar|
156
190
  # avoid freezing debug option, as when it's set, it's usually an
@@ -172,6 +206,7 @@ module HTTPX
172
206
  super || options_equals?(other)
173
207
  end
174
208
 
209
+ # checks whether +other+ is equal by comparing the session options
175
210
  def options_equals?(other, ignore_ivars = REQUEST_BODY_IVARS)
176
211
  # headers and other request options do not play a role, as they are
177
212
  # relevant only for the request.
@@ -187,6 +222,8 @@ module HTTPX
187
222
  end
188
223
  end
189
224
 
225
+ # returns a HTTPX::Options instance resulting of the merging of +other+ with self.
226
+ # it may return self if +other+ is self or equal to self.
190
227
  def merge(other)
191
228
  if (is_options = other.is_a?(Options))
192
229
 
@@ -361,7 +398,7 @@ module HTTPX
361
398
  request_class response_class headers_class request_body_class
362
399
  response_body_class connection_class http1_class http2_class
363
400
  resolver_native_class resolver_system_class resolver_https_class options_class pool_class
364
- io fallback_protocol debug debug_redact resolver_class
401
+ io fallback_protocol debug debug_redact
365
402
  compress_request_body decompress_response_body
366
403
  persistent close_on_fork
367
404
  ].each do |method_name|
@@ -421,6 +458,41 @@ module HTTPX
421
458
  Array(value)
422
459
  end
423
460
 
461
+ def option_resolver_class(resolver_type)
462
+ case resolver_type
463
+ when Symbol
464
+ meth = :"resolver_#{resolver_type}_class"
465
+
466
+ raise TypeError, ":resolver_class must be a supported type" unless respond_to?(meth)
467
+
468
+ resolver_type
469
+ when Class
470
+ raise TypeError, ":resolver_class must be a subclass of `#{Resolver::Resolver}`" unless resolver_type < Resolver::Resolver
471
+
472
+ resolver_type
473
+ else
474
+ raise TypeError, ":resolver_class must be a supported type"
475
+ end
476
+ end
477
+
478
+ def option_resolver_cache(cache_type)
479
+ if cache_type.is_a?(Symbol)
480
+ raise TypeError, ":resolver_cache: #{cache_type} is invalid" unless RESOLVER_TYPES.include?(cache_type)
481
+
482
+ require "httpx/resolver/cache/file" if cache_type == :file
483
+
484
+ else
485
+ unless cache_type.respond_to?(:resolve) &&
486
+ cache_type.respond_to?(:get) &&
487
+ cache_type.respond_to?(:set) &&
488
+ cache_type.respond_to?(:evict)
489
+ raise TypeError, ":resolver_cache must be a compatible resolver cache and implement #resolve, #get, #set and #evict"
490
+ end
491
+ end
492
+
493
+ cache_type
494
+ end
495
+
424
496
  # called after all options are initialized
425
497
  def do_initialize
426
498
  hs = @headers
@@ -496,6 +568,7 @@ module HTTPX
496
568
  :addresses => nil,
497
569
  :persistent => false,
498
570
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
571
+ :resolver_cache => (ENV["HTTPX_RESOLVER_CACHE"] || :memory).to_sym,
499
572
  :resolver_options => { cache: true }.freeze,
500
573
  :pool_options => EMPTY_HASH,
501
574
  :ip_families => nil,
@@ -26,6 +26,7 @@ module HTTPX
26
26
  @headers = {}
27
27
  @content_length = nil
28
28
  @_has_trailers = nil
29
+ @buffer.clear
29
30
  end
30
31
 
31
32
  def upgrade?
@@ -16,6 +16,13 @@ module HTTPX
16
16
  }
17
17
  end
18
18
 
19
+ # adds support for the following options:
20
+ #
21
+ # :auth_header_value :: the token to use as a string, or a callable which returns a string when called.
22
+ # :auth_header_type :: the authentication type to use in the "authorization" header value (i.e. "Bearer", "Digest"...)
23
+ # :generate_auth_value_on_retry :: callable which returns whether the request should regenerate the auth_header_value
24
+ # when the request is retried (this option will only work if the session also loads the
25
+ # <tt>:retries</tt> plugin).
19
26
  module OptionsMethods
20
27
  def option_auth_header_value(value)
21
28
  value
@@ -74,10 +81,14 @@ module HTTPX
74
81
  def generate_auth_token
75
82
  return unless (auth_value = @options.auth_header_value)
76
83
 
77
- auth_value = auth_value.call(self) if auth_value.respond_to?(:call)
84
+ auth_value = auth_value.call(self) if dynamic_auth_token?(auth_value)
78
85
 
79
86
  auth_value
80
87
  end
88
+
89
+ def dynamic_auth_token?(auth_header_value)
90
+ auth_header_value&.respond_to?(:call)
91
+ end
81
92
  end
82
93
 
83
94
  module RequestMethods
@@ -92,14 +103,29 @@ module HTTPX
92
103
 
93
104
  module AuthRetries
94
105
  module InstanceMethods
106
+ private
107
+
108
+ def retryable_request?(request, response, options)
109
+ super || auth_error?(response, options)
110
+ end
111
+
112
+ def retryable_response?(response, options)
113
+ auth_error?(response, options) || super
114
+ end
115
+
95
116
  def prepare_to_retry(request, response)
96
117
  super
97
118
 
98
- return unless @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
119
+ return unless auth_error?(response, request.options) ||
120
+ (@options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response))
99
121
 
100
122
  request.headers.get("authorization").pop
101
123
  @auth_header_value = generate_auth_token
102
124
  end
125
+
126
+ def auth_error?(response, options)
127
+ response.is_a?(Response) && response.status == 401 && dynamic_auth_token?(options.auth_header_value)
128
+ end
103
129
  end
104
130
  end
105
131
  end
@@ -180,6 +180,10 @@ module HTTPX
180
180
  end
181
181
  end
182
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.
183
187
  module OptionsMethods
184
188
  private
185
189
 
@@ -255,29 +259,18 @@ module HTTPX
255
259
 
256
260
  @oauth_session.fetch_access_token(self)
257
261
  end
258
- end
259
262
 
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
268
-
269
- def response_oauth_error?(res)
270
- res.is_a?(Response) && res.status == 401
271
- end
263
+ def dynamic_auth_token?(_)
264
+ @oauth_session
272
265
  end
266
+ end
273
267
 
268
+ module OAuthRetries
274
269
  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
270
+ private
279
271
 
280
- @oauth_session.reset!
272
+ def prepare_to_retry(_request, response)
273
+ @oauth_session.reset! if @oauth_session
281
274
 
282
275
  super
283
276
  end
@@ -55,10 +55,8 @@ module HTTPX
55
55
 
56
56
  private
57
57
 
58
- def repeatable_request?(request, _)
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) || @options.max_retries != 1)
74
+ (!ex.is_a?(RequestTimeoutError) || options.max_retries != 1)
77
75
  end
78
76
  end
79
77
  end
@@ -43,10 +43,6 @@ module HTTPX
43
43
  end
44
44
 
45
45
  module ConnectionMethods
46
- def connecting?
47
- super || @state == :connecting || @state == :connected
48
- end
49
-
50
46
  def force_close(*)
51
47
  if @state == :connecting
52
48
  # proxy connect related requests should not be reenqueed
@@ -326,7 +326,9 @@ module HTTPX
326
326
 
327
327
  module ProxyRetries
328
328
  module InstanceMethods
329
- def retryable_error?(ex)
329
+ private
330
+
331
+ def retryable_error?(ex, *)
330
332
  super || ex.is_a?(ProxyConnectionError)
331
333
  end
332
334
  end
@@ -23,7 +23,7 @@ module HTTPX
23
23
  module InstanceMethods
24
24
  private
25
25
 
26
- def repeatable_request?(request, options)
26
+ def retryable_request?(request, *)
27
27
  super || request.verb == "QUERY"
28
28
  end
29
29
  end
@@ -12,22 +12,11 @@ module HTTPX
12
12
  # https://gitlab.com/os85/httpx/wikis/Rate-Limiter
13
13
  #
14
14
  module RateLimiter
15
- class << self
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
- status = response.status
29
-
30
- RATE_LIMIT_CODES.include?(status)
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
@@ -142,15 +142,8 @@ module HTTPX
142
142
 
143
143
  if response &&
144
144
  request.retries.positive? &&
145
- repeatable_request?(request, options) &&
146
- (
147
- (
148
- response.is_a?(ErrorResponse) && retryable_error?(response.error)
149
- ) ||
150
-
151
- options.retry_on&.call(response)
152
-
153
- )
145
+ retryable_request?(request, response, options) &&
146
+ retryable_response?(response, options)
154
147
  try_partial_retry(request, response)
155
148
  log { "failed to get response, #{request.retries} tries to go..." }
156
149
  prepare_to_retry(request, response)
@@ -186,12 +179,16 @@ module HTTPX
186
179
  end
187
180
 
188
181
  # returns whether +request+ can be retried.
189
- def repeatable_request?(request, options)
182
+ def retryable_request?(request, _, options)
190
183
  IDEMPOTENT_METHODS.include?(request.verb) || options.retry_change_requests
191
184
  end
192
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
+
193
190
  # returns whether the +ex+ exception happend for a retriable request.
194
- def retryable_error?(ex)
191
+ def retryable_error?(ex, _)
195
192
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
196
193
  end
197
194
 
@@ -164,7 +164,7 @@ module HTTPX
164
164
 
165
165
  unless request.options.stream && !request.stream
166
166
  if options[:stream]
167
- warn "passing `stream: true` with a request obkect is not supported anymore. " \
167
+ warn "passing `stream: true` with a request object is not supported anymore. " \
168
168
  "You can instead build the request object with `stream :true`"
169
169
  end
170
170
  return super
@@ -217,7 +217,7 @@ module HTTPX
217
217
 
218
218
  @stream.on_chunk(chunk.dup)
219
219
 
220
- chunk.size
220
+ chunk.bytesize
221
221
  end
222
222
 
223
223
  private
@@ -278,6 +278,8 @@ module HTTPX
278
278
  headers_sent = @headers_sent
279
279
 
280
280
  case nextstate
281
+ when :idle
282
+ headers_sent = false
281
283
  when :waiting_for_chunk
282
284
  return unless @state == :body
283
285
  when :body
@@ -325,7 +327,17 @@ module HTTPX
325
327
  module RequestBodyMethods
326
328
  def initialize(*, **)
327
329
  super
328
- @headers.delete("content-length") if @options.stream
330
+
331
+ return unless @options.stream
332
+
333
+ @headers.delete("content-length")
334
+
335
+ return unless @body
336
+
337
+ return if @body.is_a?(Transcoder::Body::Encoder)
338
+
339
+ raise Error, "bidirectional streams only allow the usage of the `:body` param to set request bodies." \
340
+ "You must encode it yourself if you wish to do so."
329
341
  end
330
342
 
331
343
  def empty?
data/lib/httpx/pool.rb CHANGED
@@ -148,7 +148,6 @@ module HTTPX
148
148
 
149
149
  def checkout_resolver(options)
150
150
  resolver_type = options.resolver_class
151
- resolver_type = Resolver.resolver_for(resolver_type, options)
152
151
 
153
152
  @resolver_mtx.synchronize do
154
153
  resolvers = @resolvers[resolver_type]
@@ -128,7 +128,7 @@ module HTTPX
128
128
  Transcoder::Body.encode(body)
129
129
  elsif (form = params.delete(:form))
130
130
  if Transcoder::Multipart.multipart?(form)
131
- # @type var form: Transcoder::multipart_input
131
+ # @type var form: Transcoder::Multipart::multipart_input
132
132
  Transcoder::Multipart.encode(form)
133
133
  else
134
134
  # @type var form: Transcoder::urlencoded_input