httpx 1.8.1 → 1.8.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eece37bd091a7fea17ccc62eca63acaee714c08658d081a938fc38495a4c8813
4
- data.tar.gz: 72dcd038803106f08a0317c8d7bc97d22d6cdf5c469ba8f8f9d380d00e8f5ea7
3
+ metadata.gz: c34100ea496b94c4dbcfcd95330a5c8fa21dd5997d49dff54f62ced363589f7f
4
+ data.tar.gz: 24ef436a40b17cf4df3b2a9722c36437704c0c2ed1f5a1afc59b16801c36dbb4
5
5
  SHA512:
6
- metadata.gz: 5af43571f7da30f709883474fb61ef3661b31fa836d249c423726f4815fb7b2bf5c1281e22ca7ad15c00ed26b2ffc950d521b14a9f5c02e7d95d4198b4b986d0
7
- data.tar.gz: bd73537be9a610b590d7897cd96f7b6213ece24ea7079ce995a3e05cbee363dcf0f575405ea8d2e612a64737e3bbc70dbdc25ab9a24f5b7e154b9a5187974ffe
6
+ metadata.gz: 7796301721da3f362b4ec13bf5bffcdec864819dddf404dfb7c7ffadde69508de32e52b85e163b144d4ade6b3579aa256107680332a264b27cbe67fdd9e23875
7
+ data.tar.gz: 63f47f3e7c9e50b720c8637dabc506fe30e79fb403e6d658646ff6fb894dc02774c8e1c2491d938392f1184d8f26f681361f9ffd1dec6675a622c03ba0441342
@@ -0,0 +1,30 @@
1
+ # 1.8.2
2
+
3
+ ## Improvements
4
+
5
+ * `:proxy` plugin: allow setting `:no_proxy` option to `"*"` (or `["*"]`) to allow bypassing proxy for all requests.
6
+ * Extending sessions with unknown options will now raise an error, instead of silently ignoring them (consistent with the initialization case now):
7
+
8
+ ```ruby
9
+ session = HTTPX.with(unknown_option: 1) # will raise a HTTPX::Error: unknown option: unknown_option
10
+ ```
11
+
12
+ * Use `HTTP2::Connection#closed?` internally (instead of `#state`) to check for connection state; this will, in a future release of `http-2`, seamlessly support the new `:closing` state.
13
+ * Add missing `response` attribute reader to `HTTPX::RequestTimeoutError`.
14
+ * Improve correctness of calling `#dup` on requests and connections.
15
+
16
+ ## Bugfixes
17
+
18
+ * `:auth` plugin: generate token first, then set the "expires_at" timestamp (bearer tokens, specially if JWT, may carry information about when it expires).
19
+ * `:auth` plugin: do not check for token freshness on a retry, instead force token regeneration.
20
+ * `:retries` plugin: do not apply "retry after" delay after a ping timeout error (the ping timeout already implies a delay, so retry on a new connection should be immediate).
21
+ * `:retries` plugin: do not retry on any HTTP/2 frame error (only on GOAWAY or PING frame errors).
22
+ * `:persistent` plugin: do not decrement request remaining retries on ping or GOAWAY frame errors.
23
+ * `:tracing` plugin: set connection initialization time to nil no termination (so that it doesn't get reused on persistent connections after reuse).
24
+ * `:follow_redirects` plugin: fixed issue when a timeout error would be raised but only bubble up to the root request, not properly closing the in-flight follow up request, and leaving the request handling phase in a broken state.
25
+ * datadog adapter: the connection initialization time is now set only on connect or first request received, the reason, making it more accurate.
26
+ * webmock adapter: `.to_timeout` helper will make the request raise a `HTTPX::RequestTimeoutError` instead of a plain `TimeoutError` (otherwise, if using persistent sessions, it'd trigger a retry instead and mess with webmock request count bookkeeping).
27
+ * native resolver: do not transition to `:open` if the socket was closed during resolution (which happens when an error is raised while the query bytes are buffered).
28
+ * fix for when setting a new session callback on an ssl context object which has been frozen after the first connection attempt (done by binding the ssl context callback to an internal ivar which can be changed).
29
+ * connection: protect against `Errno::ETIMEDOUT` errors on socket close by forcing transition to closed.
30
+ * do not set a ping timer for HTTP/1.1 on keep alive flow, as there's no ping and the connection is reestablished.
@@ -34,7 +34,8 @@ module WebMock
34
34
  end
35
35
 
36
36
  def build_from_webmock_response(request, webmock_response)
37
- return build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
37
+ return build_error_response(request,
38
+ HTTPX::RequestTimeoutError.new(request, webmock_response, 1)) if webmock_response.should_timeout
38
39
 
39
40
  return build_error_response(request, webmock_response.exception) if webmock_response.exception
40
41
 
data/lib/httpx/buffer.rb CHANGED
@@ -46,6 +46,11 @@ module HTTPX
46
46
  def_delegator :@buffer, :<<
47
47
  end
48
48
 
49
+ def initialize_dup(other)
50
+ super
51
+ @buffer = other.instance_variable_get(:@buffer).dup
52
+ end
53
+
49
54
  def full?
50
55
  @buffer.bytesize >= @limit
51
56
  end
@@ -29,7 +29,7 @@ module HTTPX
29
29
  @version = [1, 1]
30
30
  @pending = []
31
31
  @requests = []
32
- @request = nil
32
+ @callbacks = @request = nil
33
33
  @handshake_completed = @pipelining = false
34
34
  end
35
35
 
@@ -31,6 +31,7 @@ module HTTPX
31
31
  attr_reader :streams, :pending
32
32
 
33
33
  def initialize(buffer, options)
34
+ @callbacks = nil
34
35
  @options = options
35
36
  @settings = @options.http2_settings
36
37
  @pending = []
@@ -53,7 +54,7 @@ module HTTPX
53
54
  end
54
55
 
55
56
  def interests
56
- if @connection.state == :closed
57
+ if @connection.closed?
57
58
  return unless @handshake_completed
58
59
 
59
60
  return if @buffer.empty?
@@ -94,7 +95,7 @@ module HTTPX
94
95
  end
95
96
 
96
97
  def close
97
- unless @connection.state == :closed
98
+ unless @connection.closed?
98
99
  @connection.goaway
99
100
  emit(:timeout, @options.timeout[:close_handshake_timeout])
100
101
  end
@@ -102,7 +103,7 @@ module HTTPX
102
103
  end
103
104
 
104
105
  def empty?
105
- @connection.state == :closed || @streams.empty?
106
+ @connection.closed? || @streams.empty?
106
107
  end
107
108
 
108
109
  def exhausted?
@@ -432,7 +433,7 @@ module HTTPX
432
433
  end
433
434
 
434
435
  def on_close(_last_frame, error, _payload)
435
- is_connection_closed = @connection.state == :closed
436
+ is_connection_closed = @connection.closed?
436
437
  if error
437
438
  @buffer.clear if is_connection_closed
438
439
  case error
@@ -36,19 +36,20 @@ module HTTPX
36
36
 
37
37
  def_delegator :@write_buffer, :empty?
38
38
 
39
- attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling
39
+ attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling,
40
+ :read_buffer, :write_buffer
40
41
 
41
42
  attr_writer :current_selector
42
43
 
43
44
  attr_accessor :current_session, :family
44
45
 
45
- protected :ssl_session, :sibling
46
+ protected :ssl_session, :sibling, :read_buffer, :write_buffer
46
47
 
47
48
  def initialize(uri, options)
48
49
  @current_session = @current_selector = @max_concurrent_requests =
49
50
  @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
- @ping_timer = @family = @io = @ssl_session =
51
- @timeout = @connected_at = @response_received_at = nil
51
+ @callbacks = @ping_timer = @family =
52
+ @io = @ssl_session = @timeout = @connected_at = @response_received_at = nil
52
53
 
53
54
  @exhausted = @cloned = @main_sibling = false
54
55
 
@@ -76,6 +77,29 @@ module HTTPX
76
77
  self.addresses = @options.addresses if @options.addresses
77
78
  end
78
79
 
80
+ # dupped initialization
81
+ def initialize_dup(orig)
82
+ super
83
+ @callbacks = @parser = @sibling = @coalesced_connection = @altsvc_connection = nil
84
+ @origins = orig.origins.dup
85
+ @read_buffer = orig.read_buffer.dup
86
+ @write_buffer = orig.write_buffer.dup
87
+ @inflight = 0
88
+ @pending = []
89
+ transition(:idle)
90
+
91
+ if @io
92
+ # initialize new IO object with the same set of addresses
93
+ addresses = @io.addresses
94
+ @io = nil
95
+ self.addresses = addresses
96
+ end
97
+
98
+ return unless @current_session && @current_selector
99
+
100
+ @current_session.pin(self, @current_selector)
101
+ end
102
+
79
103
  def peer
80
104
  @origin
81
105
  end
@@ -148,11 +172,11 @@ module HTTPX
148
172
 
149
173
  def merge(connection)
150
174
  @origins |= connection.instance_variable_get(:@origins)
151
- if @ssl_session.nil? && connection.ssl_session
152
- @ssl_session = connection.ssl_session
153
- @io.session_new_cb do |sess|
154
- @ssl_session = sess
155
- end if @io
175
+ if @ssl_session.nil? && (ssl_session = connection.ssl_session)
176
+ @ssl_session = ssl_session
177
+ # the socket only needs the merged session if it can still resume it,
178
+ # i.e. if TLS hasn't been negotiated yet.
179
+ @io.ssl_session = ssl_session if @io.is_a?(SSL) && !@io.connected?
156
180
  end
157
181
  connection.purge_pending do |req|
158
182
  req.transition(:idle)
@@ -341,7 +365,6 @@ module HTTPX
341
365
  end
342
366
 
343
367
  transition(:closing)
344
-
345
368
  transition(:closed)
346
369
  end
347
370
 
@@ -775,14 +798,19 @@ module HTTPX
775
798
  Errno::ECONNRESET,
776
799
  Errno::EADDRNOTAVAIL,
777
800
  Errno::EHOSTUNREACH,
778
- Errno::EINVAL,
779
801
  Errno::ENETUNREACH,
802
+ Errno::EHOSTDOWN,
803
+ Errno::ENETDOWN,
804
+ Errno::EINVAL,
780
805
  Errno::EPIPE,
781
806
  Errno::ENOENT,
782
807
  SocketError,
783
808
  IOError => e
784
809
  on_connect_error(e)
785
- rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
810
+ rescue TLSError,
811
+ Errno::ETIMEDOUT,
812
+ ::HTTP2::Error::ProtocolError,
813
+ ::HTTP2::Error::HandshakeError => e
786
814
  # connect errors, exit gracefully
787
815
  handle_error(e)
788
816
  handle_connect_error(e) if connecting?
@@ -979,6 +1007,8 @@ module HTTPX
979
1007
 
980
1008
  parser.ping
981
1009
 
1010
+ return unless parser.waiting_for_ping?
1011
+
982
1012
  ping_timeout = @options.timeout[:ping_timeout]
983
1013
 
984
1014
  @ping_timer = @current_selector.after(ping_timeout) do
data/lib/httpx/errors.rb CHANGED
@@ -43,6 +43,9 @@ module HTTPX
43
43
  # The HTTPX::Request request object this exception refers to.
44
44
  attr_reader :request
45
45
 
46
+ # The response object this exception refers to.
47
+ attr_reader :response
48
+
46
49
  # initializes the exception with the +request+ and +response+ it refers to, and the
47
50
  # +timeout+ causing the error, and the
48
51
  def initialize(request, response, timeout)
data/lib/httpx/io/ssl.rb CHANGED
@@ -6,20 +6,19 @@ module HTTPX
6
6
  TLSError = OpenSSL::SSL::SSLError
7
7
 
8
8
  class SSL < TCP
9
- # rubocop:disable Style/MutableConstant
10
- TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
11
- # https://github.com/jruby/jruby-openssl/issues/284
9
+ tls_options = { alpn_protocols: %w[h2 http/1.1].freeze }
12
10
  # TODO: remove when dropping support for jruby-openssl < 0.15.4
13
- TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby" && JOpenSSL::VERSION < "0.15.4"
14
- # rubocop:enable Style/MutableConstant
15
- TLS_OPTIONS.freeze
11
+ # https://github.com/jruby/jruby-openssl/issues/284
12
+ tls_options[:verify_hostname] = true if RUBY_ENGINE == "jruby" && JOpenSSL::VERSION < "0.15.4"
13
+ TLS_OPTIONS = tls_options.freeze
16
14
 
17
15
  attr_writer :ssl_session
18
16
 
19
17
  def initialize(_, _, options)
20
18
  super
21
19
 
22
- @ssl_session = nil
20
+ @ssl_session = @session_new_cb = nil
21
+
23
22
  ctx_options = TLS_OPTIONS
24
23
  ctx_options = ctx_options.merge(options.ssl) if options.ssl && !options.ssl.empty?
25
24
  @sni_hostname = (ctx_options.delete(:hostname) if ctx_options.key?(:hostname)) || @hostname
@@ -35,6 +34,7 @@ module HTTPX
35
34
  @ctx.session_cache_mode =
36
35
  OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
37
36
  end
37
+ init_session_new_cb
38
38
 
39
39
  yield(self) if block_given?
40
40
  end
@@ -43,14 +43,24 @@ module HTTPX
43
43
  end
44
44
 
45
45
  if OpenSSL::SSL::SSLContext.method_defined?(:session_new_cb=)
46
+ # sets the ssl session callback to be picked up by the ssl context.
46
47
  def session_new_cb(&pr)
47
- @ctx.session_new_cb = proc { |_, sess| pr.call(sess) }
48
+ @session_new_cb = pr
49
+ end
50
+
51
+ # sets the ssl context's new session callback, which points at @session_new_cb when available.
52
+ def init_session_new_cb
53
+ @ctx.session_new_cb = proc { |_, sess| @session_new_cb&.call(sess) }
48
54
  end
49
55
  else
50
56
  # session_new_cb not implemented under JRuby
51
57
  def session_new_cb; end
58
+
59
+ def init_session_new_cb; end
52
60
  end
53
61
 
62
+ private :init_session_new_cb
63
+
54
64
  def protocol
55
65
  return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
56
66
 
data/lib/httpx/options.rb CHANGED
@@ -257,12 +257,16 @@ module HTTPX
257
257
 
258
258
  other_opts = opts_names
259
259
  else
260
- other_opts = other # : Hash[Symbol, untyped]
260
+ other_opts = other #: Hash[Symbol, untyped]
261
261
  other_opts = Hash[other] unless other.is_a?(Hash)
262
262
 
263
263
  return self if other_opts.empty?
264
264
 
265
- return self if other_opts.all? { |opt, v| !respond_to?(opt) || public_send(opt) == v }
265
+ return self if other_opts.all? do |opt, v|
266
+ raise Error, "unknown option: #{opt}" unless respond_to?(opt)
267
+
268
+ public_send(opt) == v
269
+ end
266
270
  end
267
271
 
268
272
  opts = dup
@@ -19,6 +19,8 @@ module HTTPX
19
19
  # adds support for the following options:
20
20
  #
21
21
  # :auth_header_value :: the token to use as a string, or a callable which returns a string when called.
22
+ # the callable is called a request, and a boolean: when true, the user must regenerate a token, otherwise it
23
+ # may probe for token freshness and return the same token.
22
24
  # :auth_header_type :: the authentication type to use in the "authorization" header value (i.e. "Bearer", "Digest"...)
23
25
  # :auth_header_expires_at :: timestamp at which the auth header will be discarded. should be a callable (like a proc)
24
26
  # receiving the request as an argument, and should return either a Time object, or an integer (UNIX time).
@@ -96,9 +98,8 @@ module HTTPX
96
98
  auth_header_value = @auth_header_value_mtx.synchronize do
97
99
  try_invalidate_auth_header_value
98
100
 
99
- @auth_header_value ||= begin
101
+ @auth_header_value ||= generate_auth_token(request, false).tap do
100
102
  set_auth_header_expires_at(request)
101
- generate_auth_token
102
103
  end
103
104
  end
104
105
 
@@ -115,10 +116,10 @@ module HTTPX
115
116
  @auth_header_value = @auth_header_expires_at = nil
116
117
  end
117
118
 
118
- def generate_auth_token
119
+ def generate_auth_token(request, should_regenerate)
119
120
  return unless (auth_value = @options.auth_header_value)
120
121
 
121
- auth_value = auth_value.call(self) if dynamic_auth_token?(auth_value)
122
+ auth_value = auth_value.call(request, should_regenerate) if dynamic_auth_token?(auth_value)
122
123
 
123
124
  auth_value
124
125
  end
@@ -196,7 +197,7 @@ module HTTPX
196
197
  # use whatever was generated for it.
197
198
  @auth_header_value_mtx.synchronize do
198
199
  if request.auth_token_value == @auth_header_value
199
- @auth_header_value = generate_auth_token
200
+ @auth_header_value = generate_auth_token(request, true)
200
201
  set_auth_header_expires_at(request)
201
202
  end
202
203
  end
@@ -135,9 +135,9 @@ module HTTPX
135
135
  return ErrorResponse.new(request, error)
136
136
  end
137
137
 
138
- retry_request = build_request(redirect_method, redirect_uri, redirect_params, options)
138
+ redirect_request = build_request(redirect_method, redirect_uri, redirect_params, options)
139
139
 
140
- request.redirect_request = retry_request
140
+ request.redirect_request = redirect_request
141
141
 
142
142
  redirect_after = response.headers["retry-after"]
143
143
 
@@ -151,24 +151,24 @@ module HTTPX
151
151
  redirect_after = Utils.parse_retry_after(redirect_after)
152
152
 
153
153
  retry_start = Utils.now
154
- log { "redirecting after #{redirect_after} secs..." }
154
+ redirect_request.log { "redirecting after #{redirect_after} secs..." }
155
155
  selector.after(redirect_after) do
156
156
  if (response = request.response)
157
157
  response.finish!
158
- retry_request.response = response
158
+ redirect_request.response = response
159
159
  # request has terminated abruptly meanwhile
160
- retry_request.emit_response(response)
160
+ redirect_request.emit_response(response)
161
161
  else
162
- log { "redirecting (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
163
- send_request(retry_request, selector, options)
162
+ redirect_request.log { "redirecting (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
163
+ send_request(redirect_request, selector, options)
164
164
  end
165
165
  end
166
166
  else
167
- send_request(retry_request, selector, options)
167
+ send_request(redirect_request, selector, options)
168
168
 
169
169
  # recalling itself, in case an error was triggered by the above, and we can
170
170
  # verify retriability again.
171
- return fetch_response(request, selector, options)
171
+ return fetch_response(redirect_request, selector, options)
172
172
  end
173
173
  nil
174
174
  end
@@ -231,6 +231,18 @@ module HTTPX
231
231
  @redirect_request.response
232
232
  end
233
233
 
234
+ def response=(response)
235
+ return super unless @redirect_request && @response.nil? # rubocop:disable Lint/ReturnInVoidContext
236
+
237
+ @redirect_request.response = response
238
+ end
239
+
240
+ def emit_response(response)
241
+ return super unless @redirect_request && @response.nil?
242
+
243
+ @redirect_request.emit_response(response)
244
+ end
245
+
234
246
  def max_redirects
235
247
  @options.max_redirects || MAX_REDIRECTS
236
248
  end
@@ -296,9 +296,11 @@ module HTTPX
296
296
 
297
297
  private
298
298
 
299
- def generate_auth_token
300
- return unless @oauth_session
299
+ def generate_auth_token(*)
300
+ return super unless @oauth_session
301
301
 
302
+ # should_regenerate arg ignored, as there's no way to check
303
+ # for token expiration yet.
302
304
  @oauth_session.fetch_access_token(self)
303
305
  end
304
306
 
@@ -18,6 +18,12 @@ module HTTPX
18
18
  # https://gitlab.com/os85/httpx/wikis/Persistent
19
19
  #
20
20
  module Persistent
21
+ SAFE_REONNECTABLE_ERRORS = [
22
+ PingTimeoutError,
23
+ Connection::HTTP2::GoawayError,
24
+ Connection::HTTP2::PingError,
25
+ ].freeze
26
+
21
27
  class << self
22
28
  def load_dependencies(klass)
23
29
  klass.plugin(:fiber_concurrency)
@@ -59,6 +65,15 @@ module HTTPX
59
65
  Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
60
66
  end
61
67
 
68
+ # whether the error can be safely retried without booking threshold attempts.
69
+ def safe_reconnectable_error?(error)
70
+ SAFE_REONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
71
+ end
72
+
73
+ def can_reconnect?(_, response)
74
+ super || (response.is_a?(ErrorResponse) && safe_reconnectable_error?(response.error))
75
+ end
76
+
62
77
  def when_to_retry(request, response, *)
63
78
  return super unless response.is_a?(ErrorResponse)
64
79
 
@@ -182,9 +182,11 @@ module HTTPX
182
182
  if (no_proxy = proxy.no_proxy)
183
183
  no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
184
184
 
185
- # TODO: setting proxy to nil leaks the connection object in the pool
186
- return super(request_uri, selector, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
187
- next_proxy.port, no_proxy)
185
+ unless no_proxy != "*" && # NO_PROXY=* bypasses proxy use
186
+ URI::Generic.use_proxy?(request_uri.host, next_proxy.host, next_proxy.port, no_proxy)
187
+ # TODO: setting proxy to nil leaks the connection object in the pool
188
+ return super(request_uri, selector, options.merge(proxy: nil))
189
+ end
188
190
  end
189
191
 
190
192
  super(request_uri, selector, options.merge(proxy: proxy))
@@ -341,6 +343,10 @@ module HTTPX
341
343
  module InstanceMethods
342
344
  private
343
345
 
346
+ def proxy_error?(request, response, _)
347
+ super && !request.retries.positive?
348
+ end
349
+
344
350
  def retryable_error?(ex, *)
345
351
  super || ex.is_a?(ProxyConnectionError)
346
352
  end
@@ -29,7 +29,10 @@ module HTTPX
29
29
  Errno::ETIMEDOUT,
30
30
  ConnectionError,
31
31
  TLSError,
32
- Connection::HTTP2::Error,
32
+ Zlib::BufError,
33
+ PingTimeoutError,
34
+ Connection::HTTP2::GoawayError,
35
+ Connection::HTTP2::PingError,
33
36
  ].freeze
34
37
 
35
38
  RETRYABLE_ERRORS = (RECONNECTABLE_ERRORS + [
@@ -148,10 +151,13 @@ module HTTPX
148
151
  retryable_response?(response, options)
149
152
  try_partial_retry(request, response)
150
153
  log { "failed to get response, #{request.retries} tries to go..." }
151
- prepare_to_retry(request, response)
152
154
 
153
- if (retry_after = when_to_retry(request, response, options)) && retry_after.positive?
155
+ # retry-after must be calculated before prepare_to_retry, as it relies on
156
+ # state changed byit.
157
+ retry_after = when_to_retry(request, response, options)
158
+ prepare_to_retry(request, response)
154
159
 
160
+ if retry_after&.positive?
155
161
  retry_start = Utils.now
156
162
  log { "retrying after #{retry_after} secs..." }
157
163
  selector.after(retry_after) do
@@ -191,13 +197,14 @@ module HTTPX
191
197
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) } && !ex.is_a?(TotalRequestTimeoutError)
192
198
  end
193
199
 
194
- def proxy_error?(request, response, _)
195
- super && !request.retries.positive?
200
+ def prepare_to_retry(request, response)
201
+ request.retries -= 1 unless can_reconnect?(request, response)
202
+ request.transition(:idle)
196
203
  end
197
204
 
198
- def prepare_to_retry(request, _response)
199
- request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
200
- request.transition(:idle)
205
+ # do not exhaust retries on connection liveness probes
206
+ def can_reconnect?(request, _)
207
+ request.ping?
201
208
  end
202
209
 
203
210
  def when_to_retry(request, response, options)
@@ -146,7 +146,7 @@ module HTTPX
146
146
  def when_to_retry(request, *)
147
147
  retry_after = request.last_server_sent_message&.retry_after
148
148
 
149
- retry_after / 1_000.0 if retry_after # original in milliseconds
149
+ retry_after.to_i / 1_000.0 if retry_after # original in milliseconds
150
150
 
151
151
  request.last_server_sent_message&.retry_after && super
152
152
  end
@@ -108,11 +108,13 @@ module HTTPX::Plugins
108
108
  def initialize(*)
109
109
  super
110
110
 
111
- @init_time = ::Time.now.utc
111
+ @init_time = nil
112
112
  end
113
113
 
114
114
  def send_request_to_parser(request)
115
115
  if connecting?
116
+ @init_time ||= ::Time.now.utc
117
+
116
118
  # request span timeframe should include the time it took to connect.
117
119
  request.init_time ||= @init_time
118
120
  end
@@ -123,13 +125,24 @@ module HTTPX::Plugins
123
125
  def idling
124
126
  super
125
127
 
126
- # time of initial request(s) is accounted from the moment
127
- # the connection is back to :idle, and ready to connect again.
128
- @init_time = ::Time.now.utc
128
+ @init_time = nil
129
+ end
130
+
131
+ def terminate
132
+ super
133
+
134
+ # ensure that connections which go back to the pool reset their init time.
135
+ @init_time = nil
129
136
  end
130
137
 
131
138
  private
132
139
 
140
+ def connect
141
+ @init_time ||= ::Time.now.utc
142
+
143
+ super
144
+ end
145
+
133
146
  def ping(request)
134
147
  # if a connection is probed for liveness, the request timeframe should include
135
148
  # it too.
@@ -4,14 +4,17 @@ module HTTPX
4
4
  # Implementation of the HTTP Request body as a delegator which iterates (responds to +each+) payload chunks.
5
5
  class Request::Body < SimpleDelegator
6
6
  class << self
7
- def new(_, options, body: nil, **params)
8
- if body.is_a?(self)
7
+ def new(h, options, body: nil, **params)
8
+ case body
9
+ when self
9
10
  # request derives its options from body
10
11
  body.options = options.merge(params)
11
- return body
12
+ body
13
+ when nil
14
+ super(h, options, **params)
15
+ else
16
+ super
12
17
  end
13
-
14
- super
15
18
  end
16
19
  end
17
20
 
data/lib/httpx/request.rb CHANGED
@@ -105,8 +105,8 @@ module HTTPX
105
105
 
106
106
  @state = :idle
107
107
  @connection = @response =
108
- @drainer = @peer_address =
109
- @informational_status = @on_response_arrived = nil
108
+ @drainer = @peer_address = @callbacks =
109
+ @informational_status = @on_response_arrived = nil
110
110
  @ping = @started = false
111
111
  @persistent = @options.persistent
112
112
  @active_timeouts = []
@@ -115,9 +115,9 @@ module HTTPX
115
115
  # dupped initialization
116
116
  def initialize_dup(orig)
117
117
  super
118
- @uri = orig.instance_variable_get(:@uri).dup
119
118
  @headers = orig.instance_variable_get(:@headers).dup
120
119
  @body = orig.instance_variable_get(:@body).dup
120
+ @active_timeouts = orig.instance_variable_get(:@active_timeouts).dup
121
121
  end
122
122
 
123
123
  def complete!(response = @response)
@@ -539,6 +539,10 @@ module HTTPX
539
539
  return unless @io.connected?
540
540
 
541
541
  resolve if @queries.empty? && !@connections.empty?
542
+
543
+ # #resolve may have closed the resolver already as part of error handling.
544
+ # @fiber-switch-guard
545
+ return unless @io
542
546
  when :closed
543
547
  return if @state == :closed
544
548
 
@@ -36,10 +36,10 @@ module HTTPX
36
36
  # * HTTPX::Response::Body#read
37
37
  # * HTTPX::Response::Body#copy_to
38
38
  # * HTTPX::Response::Body#close
39
- attr_reader :body
39
+ attr_reader :body
40
40
 
41
41
  # The HTTP protocol version used to fetch the response.
42
- attr_reader :version
42
+ attr_reader :version
43
43
 
44
44
  # returns the response body buffered in a string.
45
45
  def_delegator :@body, :to_s
@@ -68,7 +68,7 @@ module HTTPX
68
68
  @headers = @options.headers_class.new(headers)
69
69
  @body = @options.response_body_class.new(self, @options)
70
70
  @finished = complete?
71
- @content_type = @content_length = nil
71
+ @callbacks = @content_type = @content_length = nil
72
72
  end
73
73
 
74
74
  # dupped initialization
@@ -56,7 +56,7 @@ module HTTPX
56
56
 
57
57
  begin
58
58
  select(timeout) do |c|
59
- c.log(level: 2) { "[#{c.state}] selected from selector##{object_id} #{" after #{timeout} secs" unless timeout.nil?}..." }
59
+ c.log(level: 2) { "[#{c.state}] selected from selector##{object_id}#{" after #{timeout} secs" unless timeout.nil?}..." }
60
60
 
61
61
  c.call
62
62
  end
data/lib/httpx/session.rb CHANGED
@@ -237,7 +237,7 @@ module HTTPX
237
237
 
238
238
  return unless response && response.finished?
239
239
 
240
- log(level: 2) { "response##{response.object_id} fetched" }
240
+ request.log(level: 2) { "response##{response.object_id} fetched" }
241
241
 
242
242
  response
243
243
  end
@@ -337,6 +337,8 @@ module HTTPX
337
337
  end
338
338
  end
339
339
 
340
+ log(level: 2) { "waiting to receive #{pending} pending requests..." }
341
+
340
342
  until pending.zero? || selector.empty?
341
343
  # loop on selector until at least one response has been received.
342
344
  waiting = true
@@ -360,7 +362,8 @@ module HTTPX
360
362
  end
361
363
  end
362
364
 
363
- raise Error, "something went wrong, responses not found and requests not resent" unless pending.zero?
365
+ raise Error, "something went wrong, #{pending} responses not found " \
366
+ "and requests not resent" unless pending.zero?
364
367
 
365
368
  responses
366
369
  end
@@ -51,7 +51,7 @@ module HTTPX::Transcoder
51
51
  method(:json_load)
52
52
  end
53
53
 
54
- # rubocop:disable Style/SingleLineMethods
54
+ # rubocop:disable-next Style/SingleLineMethods
55
55
  if defined?(MultiJson)
56
56
  def json_load(*args); MultiJson.load(*args); end
57
57
  def json_dump(*args); MultiJson.dump(*args); end
@@ -66,6 +66,5 @@ module HTTPX::Transcoder
66
66
  def json_load(*args); ::JSON.parse(*args); end
67
67
  def json_dump(*args); ::JSON.generate(*args); end
68
68
  end
69
- # rubocop:enable Style/SingleLineMethods
70
69
  end
71
70
  end
@@ -55,7 +55,7 @@ module HTTPX
55
55
 
56
56
  private
57
57
 
58
- # rubocop:disable Naming/MemoizedInstanceVariableName
58
+ # rubocop:disable-next Naming/MemoizedInstanceVariableName
59
59
  def buffer_deflate!
60
60
  return @buffer if defined?(@buffer)
61
61
 
@@ -68,7 +68,6 @@ module HTTPX
68
68
 
69
69
  @buffer = buffer
70
70
  end
71
- # rubocop:enable Naming/MemoizedInstanceVariableName
72
71
  end
73
72
  end
74
73
  end
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.8.1"
4
+ VERSION = "1.8.2"
5
5
  end
data/sig/connection.rbs CHANGED
@@ -32,10 +32,11 @@ module HTTPX
32
32
  attr_accessor current_session: Session?
33
33
  attr_accessor family: Integer?
34
34
 
35
+ attr_reader read_buffer: Buffer
36
+ attr_reader write_buffer: Buffer
37
+
35
38
 
36
39
  @window_size: Integer
37
- @read_buffer: Buffer
38
- @write_buffer: Buffer
39
40
  @inflight: Integer
40
41
  @max_concurrent_requests: Integer?
41
42
  @keep_alive_timeout: interval?
data/sig/errors.rbs CHANGED
@@ -31,9 +31,9 @@ module HTTPX
31
31
 
32
32
  class RequestTimeoutError < TimeoutError
33
33
  attr_reader request: Request
34
- attr_reader response: response?
34
+ attr_reader response: untyped
35
35
 
36
- def initialize: (Request request, response? response, interval timeout) -> void
36
+ def initialize: (Request request, untyped response, interval timeout) -> void
37
37
  end
38
38
 
39
39
  class ReadTimeoutError < RequestTimeoutError
data/sig/io/ssl.rbs CHANGED
@@ -9,6 +9,7 @@ module HTTPX
9
9
  @ctx: OpenSSL::SSL::SSLContext
10
10
  @verify_hostname: bool
11
11
  @sni_hostname: String
12
+ @session_new_cb: (^(OpenSSL::SSL::Session sess) -> void)?
12
13
 
13
14
  attr_writer ssl_session: OpenSSL::SSL::Session?
14
15
 
@@ -24,5 +25,9 @@ module HTTPX
24
25
 
25
26
  # :nocov:
26
27
  def try_ssl_connect: () -> void
28
+
29
+ private
30
+
31
+ def init_session_new_cb: () -> void
27
32
  end
28
33
  end
data/sig/plugins/auth.rbs CHANGED
@@ -1,7 +1,7 @@
1
1
  module HTTPX
2
2
  module Plugins
3
3
  module Auth
4
- type auth_header_value_type = String | ^(Request request) -> string
4
+ type auth_header_value_type = String | ^(?Request request, ?bool should_regenerate) -> string
5
5
 
6
6
  interface _AuthOptions
7
7
  def auth_header_value: () -> auth_header_value_type?
@@ -18,7 +18,7 @@ module HTTPX
18
18
  @auth_header_value_mtx: Thread::Mutex
19
19
  @skip_auth_header_value: bool
20
20
 
21
- def authorization: (?string token, ?auth_header_type: string) ?{ (Request) -> string } -> instance
21
+ def authorization: (?string token, ?auth_header_type: string) ?{ (?Request, ?bool) -> string } -> instance
22
22
 
23
23
  def bearer_auth: (?string token) ?{ (Request) -> string } -> instance
24
24
 
@@ -28,7 +28,7 @@ module HTTPX
28
28
 
29
29
  private
30
30
 
31
- def generate_auth_token: () -> String?
31
+ def generate_auth_token: (Request & RequestMethods request, bool should_regenerate) -> String?
32
32
 
33
33
  def dynamic_auth_token?: (auth_header_value_type auth_header_value) -> boolish
34
34
  end
@@ -1,6 +1,8 @@
1
1
  module HTTPX
2
2
  module Plugins
3
3
  module Persistent
4
+ SAFE_REONNECTABLE_ERRORS: Array[singleton(StandardError)]
5
+
4
6
  def self.load_dependencies: (singleton(Session)) -> void
5
7
 
6
8
  def self.extra_options: (Options) -> (Options)
@@ -9,6 +11,8 @@ module HTTPX
9
11
  private
10
12
 
11
13
  def reconnectable_error?: (StandardError error) -> bool
14
+
15
+ def safe_reconnectable_error?: (StandardError error) -> bool
12
16
  end
13
17
  end
14
18
 
@@ -45,9 +45,11 @@ module HTTPX
45
45
 
46
46
  def try_partial_retry: (retriesRequest request, retriesResponse response) -> void
47
47
 
48
- def prepare_to_retry: (Request & RequestMethods request, retriesResponse response) -> void
48
+ def prepare_to_retry: (retriesRequest request, retriesResponse response) -> void
49
49
 
50
- def when_to_retry: (Request & RequestMethods request, retriesResponse response, retriesOptions options) -> Numeric?
50
+ def can_reconnect?: (retriesRequest request, retriesResponse response) -> bool
51
+
52
+ def when_to_retry: (retriesRequest request, retriesResponse response, retriesOptions options) -> Numeric?
51
53
  end
52
54
 
53
55
  module RequestMethods
@@ -30,7 +30,7 @@ module HTTPX
30
30
  end
31
31
 
32
32
  module ConnectionMethods
33
- @init_time: Time
33
+ @init_time: Time?
34
34
  end
35
35
 
36
36
  type retriesRequest = Request & RequestMethods
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -170,6 +170,7 @@ extra_rdoc_files:
170
170
  - doc/release_notes/1_7_8.md
171
171
  - doc/release_notes/1_8_0.md
172
172
  - doc/release_notes/1_8_1.md
173
+ - doc/release_notes/1_8_2.md
173
174
  files:
174
175
  - LICENSE.txt
175
176
  - README.md
@@ -312,6 +313,7 @@ files:
312
313
  - doc/release_notes/1_7_8.md
313
314
  - doc/release_notes/1_8_0.md
314
315
  - doc/release_notes/1_8_1.md
316
+ - doc/release_notes/1_8_2.md
315
317
  - lib/httpx.rb
316
318
  - lib/httpx/adapters/datadog.rb
317
319
  - lib/httpx/adapters/faraday.rb
@@ -557,7 +559,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
557
559
  - !ruby/object:Gem::Version
558
560
  version: '0'
559
561
  requirements: []
560
- rubygems_version: 3.6.9
562
+ rubygems_version: 4.0.16
561
563
  specification_version: 4
562
564
  summary: HTTPX, to the future, and beyond
563
565
  test_files: []