httpx 1.8.0 → 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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +1 -2
  3. data/doc/release_notes/1_8_1.md +27 -0
  4. data/doc/release_notes/1_8_2.md +30 -0
  5. data/lib/httpx/adapters/datadog.rb +9 -0
  6. data/lib/httpx/adapters/faraday.rb +2 -2
  7. data/lib/httpx/adapters/webmock.rb +8 -1
  8. data/lib/httpx/buffer.rb +5 -0
  9. data/lib/httpx/connection/http1.rb +6 -12
  10. data/lib/httpx/connection/http2.rb +38 -10
  11. data/lib/httpx/connection.rb +86 -27
  12. data/lib/httpx/errors.rb +3 -0
  13. data/lib/httpx/headers.rb +2 -2
  14. data/lib/httpx/io/ssl.rb +26 -17
  15. data/lib/httpx/io/tcp.rb +11 -4
  16. data/lib/httpx/io/unix.rb +2 -2
  17. data/lib/httpx/loggable.rb +11 -4
  18. data/lib/httpx/options.rb +6 -2
  19. data/lib/httpx/plugins/auth/digest.rb +1 -1
  20. data/lib/httpx/plugins/auth.rb +17 -7
  21. data/lib/httpx/plugins/callbacks.rb +12 -0
  22. data/lib/httpx/plugins/digest_auth.rb +4 -0
  23. data/lib/httpx/plugins/fiber_concurrency.rb +25 -1
  24. data/lib/httpx/plugins/follow_redirects.rb +21 -9
  25. data/lib/httpx/plugins/oauth.rb +4 -2
  26. data/lib/httpx/plugins/persistent.rb +33 -2
  27. data/lib/httpx/plugins/proxy.rb +9 -3
  28. data/lib/httpx/plugins/push_promise.rb +2 -2
  29. data/lib/httpx/plugins/retries.rb +15 -8
  30. data/lib/httpx/plugins/server_sent_events.rb +1 -1
  31. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  32. data/lib/httpx/plugins/stream.rb +2 -2
  33. data/lib/httpx/plugins/stream_bidi.rb +2 -0
  34. data/lib/httpx/plugins/tracing.rb +17 -4
  35. data/lib/httpx/pool.rb +2 -2
  36. data/lib/httpx/request/body.rb +10 -7
  37. data/lib/httpx/request.rb +27 -12
  38. data/lib/httpx/resolver/cache/base.rb +1 -8
  39. data/lib/httpx/resolver/https.rb +20 -20
  40. data/lib/httpx/resolver/native.rb +11 -0
  41. data/lib/httpx/resolver/resolver.rb +4 -0
  42. data/lib/httpx/resolver/system.rb +5 -2
  43. data/lib/httpx/response/body.rb +2 -2
  44. data/lib/httpx/response.rb +6 -7
  45. data/lib/httpx/selector.rb +6 -1
  46. data/lib/httpx/session.rb +11 -3
  47. data/lib/httpx/session_extensions.rb +2 -2
  48. data/lib/httpx/timers.rb +3 -0
  49. data/lib/httpx/transcoder/json.rb +1 -2
  50. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  51. data/lib/httpx/version.rb +1 -1
  52. data/sig/connection/http2.rbs +3 -0
  53. data/sig/connection.rbs +8 -3
  54. data/sig/errors.rbs +2 -2
  55. data/sig/io/ssl.rbs +5 -0
  56. data/sig/loggable.rbs +4 -0
  57. data/sig/plugins/auth.rbs +7 -3
  58. data/sig/plugins/persistent.rbs +7 -0
  59. data/sig/plugins/retries.rbs +4 -2
  60. data/sig/plugins/tracing.rbs +1 -1
  61. data/sig/request.rbs +2 -3
  62. data/sig/resolver/system.rbs +3 -0
  63. data/sig/selector.rbs +2 -0
  64. data/sig/timers.rbs +2 -0
  65. metadata +6 -2
data/lib/httpx/io/ssl.rb CHANGED
@@ -6,22 +6,22 @@ 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
23
- ctx_options = TLS_OPTIONS.merge(options.ssl)
24
- @sni_hostname = ctx_options.delete(:hostname) || @hostname
20
+ @ssl_session = @session_new_cb = nil
21
+
22
+ ctx_options = TLS_OPTIONS
23
+ ctx_options = ctx_options.merge(options.ssl) if options.ssl && !options.ssl.empty?
24
+ @sni_hostname = (ctx_options.delete(:hostname) if ctx_options.key?(:hostname)) || @hostname
25
25
 
26
26
  if @keep_open && @io.is_a?(OpenSSL::SSL::SSLSocket)
27
27
  # externally initiated ssl socket
@@ -29,11 +29,12 @@ module HTTPX
29
29
  @state = :negotiated
30
30
  else
31
31
  @ctx = OpenSSL::SSL::SSLContext.new
32
- @ctx.set_params(ctx_options) unless ctx_options.empty?
32
+ @ctx.set_params(ctx_options)
33
33
  unless @ctx.session_cache_mode.nil? # a dummy method on JRuby
34
34
  @ctx.session_cache_mode =
35
35
  OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
36
36
  end
37
+ init_session_new_cb
37
38
 
38
39
  yield(self) if block_given?
39
40
  end
@@ -42,33 +43,41 @@ module HTTPX
42
43
  end
43
44
 
44
45
  if OpenSSL::SSL::SSLContext.method_defined?(:session_new_cb=)
46
+ # sets the ssl session callback to be picked up by the ssl context.
45
47
  def session_new_cb(&pr)
46
- @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) }
47
54
  end
48
55
  else
49
56
  # session_new_cb not implemented under JRuby
50
57
  def session_new_cb; end
58
+
59
+ def init_session_new_cb; end
51
60
  end
52
61
 
62
+ private :init_session_new_cb
63
+
53
64
  def protocol
54
- # @type ivar @io: OpenSSL::SSL::SSLSocket
65
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
66
+
55
67
  @io.alpn_protocol || super
56
- rescue StandardError
57
- super
58
68
  end
59
69
 
60
70
  if RUBY_ENGINE == "jruby"
61
71
  # in jruby, alpn_protocol may return ""
62
72
  # https://github.com/jruby/jruby-openssl/issues/287
63
73
  def protocol
64
- # @type ivar @io: OpenSSL::SSL::SSLSocket
74
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
75
+
65
76
  proto = @io.alpn_protocol
66
77
 
67
78
  return super if proto.nil? || proto.empty?
68
79
 
69
80
  proto
70
- rescue StandardError
71
- super
72
81
  end
73
82
  end
74
83
 
data/lib/httpx/io/tcp.rb CHANGED
@@ -182,11 +182,18 @@ module HTTPX
182
182
  end
183
183
 
184
184
  def close
185
- return if @keep_open || closed?
185
+ return if @keep_open
186
+
187
+ # mark tcp as closed, so that it can be disconnected.
188
+ # this bypasses the state machine API as not not allow the transition
189
+ # from idle to closed in normal circumstances.
190
+ @state = :closed if @state == :idle
191
+
192
+ return if closed?
186
193
 
187
194
  begin
188
195
  @io.close
189
- rescue StandardError => e
196
+ rescue IOError => e
190
197
  log { "error closing socket" }
191
198
  log { e.full_message(highlight: false) }
192
199
  ensure
@@ -212,7 +219,7 @@ module HTTPX
212
219
  @state == :idle || @state == :closed
213
220
  end
214
221
 
215
- # :nocov:
222
+ # simplecov:disable
216
223
  def inspect
217
224
  "#<#{self.class}:#{object_id} " \
218
225
  "#{@ip}:#{@port} " \
@@ -221,7 +228,7 @@ module HTTPX
221
228
  "@addresses=#{@addresses} " \
222
229
  "@state=#{@state}>"
223
230
  end
224
- # :nocov:
231
+ # simplecov:enable
225
232
 
226
233
  private
227
234
 
data/lib/httpx/io/unix.rb CHANGED
@@ -60,11 +60,11 @@ module HTTPX
60
60
  true
61
61
  end
62
62
 
63
- # :nocov:
63
+ # simplecov:disable
64
64
  def inspect
65
65
  "#<#{self.class}:#{object_id} @path=#{@path}) @state=#{@state})>"
66
66
  end
67
- # :nocov:
67
+ # simplecov:enable
68
68
 
69
69
  private
70
70
 
@@ -17,6 +17,12 @@ module HTTPX
17
17
 
18
18
  USE_DEBUG_LOG = ENV.key?("HTTPX_DEBUG")
19
19
 
20
+ def self.log_identifiers
21
+ "pid=#{Process.pid} " \
22
+ "tid=#{Thread.current.object_id} " \
23
+ "fid=#{Fiber.current.object_id}"
24
+ end
25
+
20
26
  def log(
21
27
  level: @options.debug_level,
22
28
  color: nil,
@@ -36,10 +42,9 @@ module HTTPX
36
42
  klass = klass.superclass
37
43
  end
38
44
 
39
- message = +"(time:#{Time.now.utc}, pid:#{Process.pid}, " \
40
- "tid:#{Thread.current.object_id}, " \
41
- "fid:#{Fiber.current.object_id}, " \
42
- "self:#{class_name}##{object_id}) "
45
+ message = +"(time=#{Time.now.utc} " \
46
+ "#{Loggable.log_identifiers} " \
47
+ "self=#{class_name}##{object_id}) "
43
48
  message << msg.call << "\n"
44
49
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
45
50
  debug_stream << message
@@ -49,6 +54,8 @@ module HTTPX
49
54
  log(level: level, color: color, debug_level: debug_level, debug: debug) { ex.full_message }
50
55
  end
51
56
 
57
+ private
58
+
52
59
  def log_redact_headers(text)
53
60
  log_redact(text, @options.debug_redact == :headers)
54
61
  end
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
@@ -24,7 +24,7 @@ module HTTPX
24
24
 
25
25
  def authenticate(request, authenticate)
26
26
  "Digest #{generate_header(request.verb, request.path, authenticate)}"
27
- rescue StandardError => e
27
+ rescue HTTPX::Error => e
28
28
  response = ErrorResponse.new(request, e)
29
29
  request.response = response
30
30
  request.emit_response(response)
@@ -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
@@ -127,8 +128,8 @@ module HTTPX
127
128
  @auth_header_expires_at = if (expires_in = request.options.auth_header_expires_in)
128
129
  Time.now.to_i + expires_in
129
130
  elsif (expires_at = request.options.auth_header_expires_at)
130
- if expires_at.respond_to?(:call)
131
- expires_at = expires_at.call(request).to_f
131
+ if expires_at.respond_to?(:call) && (expires_at = expires_at.call(request))
132
+ expires_at = expires_at.to_f
132
133
  raise Error, "`:auth_header_expires_at` must be positive" unless expires_at.positive?
133
134
 
134
135
  expires_at
@@ -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
@@ -204,6 +205,15 @@ module HTTPX
204
205
  request.unauthorize!
205
206
  end
206
207
 
208
+ def when_to_retry(request, response, *)
209
+ return super unless response.is_a?(Response)
210
+
211
+ # retry immediately if authentication no longer valid.
212
+ return if response.status == 401
213
+
214
+ super
215
+ end
216
+
207
217
  def auth_error?(response, options)
208
218
  response.is_a?(Response) && response.status == 401 && dynamic_auth_token?(options.auth_header_value)
209
219
  end
@@ -122,6 +122,18 @@ module HTTPX
122
122
  end
123
123
  end
124
124
 
125
+ module RequestMethods
126
+ def drain_body
127
+ super.tap do |chunk|
128
+ emit(:body_chunk, chunk) if chunk
129
+ rescue StandardError => e
130
+ # in case an error occurs in callback code
131
+ @drain_error = e
132
+ nil
133
+ end
134
+ end
135
+ end
136
+
125
137
  module ConnectionMethods
126
138
  private
127
139
 
@@ -51,9 +51,13 @@ module HTTPX
51
51
 
52
52
  if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
53
53
  request.transition(:idle)
54
+
54
55
  if (auth_header = digest.authenticate(request, probe_response.headers["www-authenticate"]))
55
56
  request.authorize(auth_header)
56
57
  end
58
+
59
+ next(request.response) if request.response
60
+
57
61
  super(request)
58
62
  else
59
63
  probe_response
@@ -70,6 +70,12 @@ module HTTPX
70
70
  )
71
71
  end
72
72
 
73
+ def initial_call
74
+ return unless current_context?
75
+
76
+ super
77
+ end
78
+
73
79
  def interests
74
80
  return if connecting? && @pending.none?(&:current_context?)
75
81
 
@@ -193,6 +199,12 @@ module HTTPX
193
199
  end
194
200
 
195
201
  module ResolverNativeMethods
202
+ def initial_call
203
+ return unless @queries.values.any?(&:current_context?) || @connections.any?(&:current_context?)
204
+
205
+ super
206
+ end
207
+
196
208
  def calculate_interests
197
209
  return if @queries.empty?
198
210
 
@@ -218,11 +230,23 @@ module HTTPX
218
230
  end
219
231
 
220
232
  module ResolverSystemMethods
233
+ def initial_call
234
+ return unless current_context?
235
+
236
+ super
237
+ end
238
+
221
239
  def interests
222
- return unless @queries.any? { |_, conn| conn.current_context? }
240
+ return unless current_context?
223
241
 
224
242
  super
225
243
  end
244
+
245
+ private
246
+
247
+ def current_context?
248
+ @queries.any? { |_, conn| conn.current_context? }
249
+ end
226
250
  end
227
251
 
228
252
  module FiberConcurrencyH2C
@@ -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)
@@ -55,13 +61,38 @@ module HTTPX
55
61
 
56
62
  private
57
63
 
64
+ def reconnectable_error?(error)
65
+ Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
66
+ end
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
+
77
+ def when_to_retry(request, response, *)
78
+ return super unless response.is_a?(ErrorResponse)
79
+
80
+ error = response.error
81
+ # allow request to be retried immediately if the request failed right after the keep alive timeout.
82
+ # the chances are, the request failed because the connect has been dropped by the peer server, so it's
83
+ # fine to reopen.
84
+ return if request.ping? && reconnectable_error?(error)
85
+
86
+ super
87
+ end
88
+
58
89
  def retryable_request?(request, response, *)
59
90
  super || begin
60
- return false unless response && response.is_a?(ErrorResponse)
91
+ return false unless response.is_a?(ErrorResponse)
61
92
 
62
93
  error = response.error
63
94
 
64
- Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
95
+ reconnectable_error?(error)
65
96
  end
66
97
  end
67
98
 
@@ -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
@@ -44,9 +44,9 @@ module HTTPX
44
44
 
45
45
  def __on_promise_request(parser, stream, h)
46
46
  log(level: 1, color: :yellow) do
47
- # :nocov:
47
+ # simplecov:disable
48
48
  h.map { |k, v| "#{stream.id}: -> PROMISE HEADER: #{k}: #{v}" }.join("\n")
49
- # :nocov:
49
+ # simplecov:enable
50
50
  end
51
51
  headers = @options.headers_class.new(h)
52
52
  path = headers[":path"]
@@ -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
@@ -149,7 +149,7 @@ module HTTPX
149
149
  SsrfFilter.unsafe_ip_address?(ipaddr) || @options.extra_unsafe_ranges&.any? { |r| r.include?(ipaddr) }
150
150
  end
151
151
 
152
- raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
152
+ raise ServerSideRequestForgeryError, "#{@origin.host} has no allowed IP addresses" if addrs.empty?
153
153
 
154
154
  super
155
155
  end
@@ -75,11 +75,11 @@ module HTTPX
75
75
  @on_chunk.call(chunk)
76
76
  end
77
77
 
78
- # :nocov:
78
+ # simplecov:disable
79
79
  def inspect
80
80
  "#<#{self.class}:#{object_id}>"
81
81
  end
82
- # :nocov:
82
+ # simplecov:enable
83
83
 
84
84
  def to_s
85
85
  if @request.response
@@ -177,6 +177,8 @@ module HTTPX
177
177
  @pipe_read.readpartial(1)
178
178
  end
179
179
 
180
+ def initial_call; end
181
+
180
182
  def interests
181
183
  return if @closed
182
184