httpx 1.8.0 → 1.8.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 (49) 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/lib/httpx/adapters/datadog.rb +9 -0
  5. data/lib/httpx/adapters/faraday.rb +2 -2
  6. data/lib/httpx/adapters/webmock.rb +6 -0
  7. data/lib/httpx/connection/http1.rb +5 -11
  8. data/lib/httpx/connection/http2.rb +33 -6
  9. data/lib/httpx/connection.rb +44 -15
  10. data/lib/httpx/headers.rb +2 -2
  11. data/lib/httpx/io/ssl.rb +8 -9
  12. data/lib/httpx/io/tcp.rb +11 -4
  13. data/lib/httpx/io/unix.rb +2 -2
  14. data/lib/httpx/loggable.rb +11 -4
  15. data/lib/httpx/plugins/auth/digest.rb +1 -1
  16. data/lib/httpx/plugins/auth.rb +11 -2
  17. data/lib/httpx/plugins/callbacks.rb +12 -0
  18. data/lib/httpx/plugins/digest_auth.rb +4 -0
  19. data/lib/httpx/plugins/fiber_concurrency.rb +25 -1
  20. data/lib/httpx/plugins/persistent.rb +18 -2
  21. data/lib/httpx/plugins/push_promise.rb +2 -2
  22. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  23. data/lib/httpx/plugins/stream.rb +2 -2
  24. data/lib/httpx/plugins/stream_bidi.rb +2 -0
  25. data/lib/httpx/pool.rb +2 -2
  26. data/lib/httpx/request/body.rb +2 -2
  27. data/lib/httpx/request.rb +24 -9
  28. data/lib/httpx/resolver/cache/base.rb +1 -8
  29. data/lib/httpx/resolver/https.rb +20 -20
  30. data/lib/httpx/resolver/native.rb +7 -0
  31. data/lib/httpx/resolver/resolver.rb +4 -0
  32. data/lib/httpx/resolver/system.rb +5 -2
  33. data/lib/httpx/response/body.rb +2 -2
  34. data/lib/httpx/response.rb +3 -4
  35. data/lib/httpx/selector.rb +5 -0
  36. data/lib/httpx/session.rb +6 -1
  37. data/lib/httpx/session_extensions.rb +2 -2
  38. data/lib/httpx/timers.rb +3 -0
  39. data/lib/httpx/version.rb +1 -1
  40. data/sig/connection/http2.rbs +3 -0
  41. data/sig/connection.rbs +5 -1
  42. data/sig/loggable.rbs +4 -0
  43. data/sig/plugins/auth.rbs +4 -0
  44. data/sig/plugins/persistent.rbs +3 -0
  45. data/sig/request.rbs +2 -3
  46. data/sig/resolver/system.rbs +3 -0
  47. data/sig/selector.rbs +2 -0
  48. data/sig/timers.rbs +2 -0
  49. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74104ce6c4d05053e2a005974de5f0cd66346a1cb4558b7c0887d3a2d691d226
4
- data.tar.gz: ca0ce3e30f509d44e4c0daac30a0b5d9949c118c19a715561c276f1debaf5dbf
3
+ metadata.gz: eece37bd091a7fea17ccc62eca63acaee714c08658d081a938fc38495a4c8813
4
+ data.tar.gz: 72dcd038803106f08a0317c8d7bc97d22d6cdf5c469ba8f8f9d380d00e8f5ea7
5
5
  SHA512:
6
- metadata.gz: 0de3c6baf56c86272cc45bdc06e1187655e759ddf329414d71fcbf4603c9d0e03791641e05f5c07ef84cd578a3bba1a855fde77b5b0f1f2e11df1cf60ea0815a
7
- data.tar.gz: a276c6ed937c8d04103382d517d56d9df805886f23b05a38f6e654fcd6935e17407101e1eedc2b6605b4f347a37262116024a61633ebdd660131b06fc1f3fde9
6
+ metadata.gz: 5af43571f7da30f709883474fb61ef3661b31fa836d249c423726f4815fb7b2bf5c1281e22ca7ad15c00ed26b2ffc950d521b14a9f5c02e7d95d4198b4b986d0
7
+ data.tar.gz: bd73537be9a610b590d7897cd96f7b6213ece24ea7079ce995a3e05cbee363dcf0f575405ea8d2e612a64737e3bbc70dbdc25ab9a24f5b7e154b9a5187974ffe
@@ -17,5 +17,4 @@
17
17
  * `:stream_bidi` plugin: fix internal state preventing bidi requests from being retried.
18
18
  * `:stream_bidi` plugin: will only allow initial request body being passed using `:body` param (others, like `:json`, will raise an exception)
19
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
- *
20
+ * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
@@ -0,0 +1,27 @@
1
+ # 1.8.1
2
+
3
+ ## Improvements
4
+
5
+ * the session will now eagerly write initial data to connections instead of waiting for I/O the first time (saves a syscall).
6
+ * removed all rescue blocks rescuing `StandardError` internally; this could interfere with usages of `httpx` inside `Timeout.timeout` blocks, which may emit an exception at any point, including in blocks where that exception could be rescued, leaving the session in a potentially irrecoverable state and looping.
7
+ * debug logs change the display of ids from 'tid:123, fid:123' to 'tid=123 fid:123'.
8
+ * and when using the `datadog` adapter, all related correlation ids (trace id, span id, etc) will also be added to the identifiers in the same format
9
+ * plugins built on top of the `:retries` plugin will skip the `retry_after` interval when certain conditions are met.
10
+ * `:persistent` plugin: request will be retried immediately, when the error happens after the keep alive interval expired.
11
+ * `:auth` plugin: request will be retried immediately if it failed due to a "401 Unauthorized Error".
12
+ * `:ssrf_filter` plugin: removed public/private references of exception message.
13
+
14
+ ## Bugfixes
15
+
16
+ * `:auth` plugin: handle edge case where `:auth_header_expires_at` returns no value.
17
+ * `:digest_auth` plugin: do not retry if there was an issue building building the authorization header after the probe.
18
+ * `webmock` adapter: prevent mocked requests from opening a connection to the peer server that will not be used.
19
+ * clear and cancel request timers on completion or reset, ensuring that they won't be retained way past the point where the request is gone.
20
+ * connection: cancel and discard ping timer before closing (removes timer reference)
21
+ * connection: fix logic where purging-while-resending requests to a coalesced connection was leaving references in the old connection.
22
+ * moved an `on_body_chunk` callback to the `:callbacks` plugin, where it's used and expected to be defined.
23
+ * `http2`: avoid closing the connection on internal errors which are supposed to be specific to a request, such as the max headers or max response body size checks.
24
+ * system resolver will use an internal exception when using `Timeout.timeout` on resolutions to avoid conflicts with the default timeout exception class.
25
+ * prevent edge case where `#alpn_protocol` was being called on a `TCPSocket`.
26
+ * fixed v1.8.0 regression causing connection to loop when an error happens while connecting.
27
+ * resolver cache: fixed cache eviction logic, where alias answers weren't being evicted, causing resolutions to stall after a certain number of resolutions.
@@ -185,6 +185,13 @@ module Datadog::Tracing
185
185
  end
186
186
  end
187
187
 
188
+ # patches httpx debug logs to include datadog correlation ids
189
+ module LoggablePatch
190
+ def log_identifiers
191
+ "#{super} #{Datadog::Tracing.log_correlation}"
192
+ end
193
+ end
194
+
188
195
  module Configuration
189
196
  # Default settings for httpx
190
197
  #
@@ -297,6 +304,8 @@ module Datadog::Tracing
297
304
 
298
305
  ::HTTPX.send(:remove_const, :Session)
299
306
  ::HTTPX.send(:const_set, :Session, datadog_session.class)
307
+
308
+ ::HTTPX::Loggable.singleton_class.prepend(LoggablePatch)
300
309
  end
301
310
  end
302
311
 
@@ -115,11 +115,11 @@ module Faraday
115
115
  ssl_options
116
116
  end
117
117
  else
118
- # :nocov:
118
+ # simplecov:disable
119
119
  def ssl_options_from_env(*)
120
120
  {}
121
121
  end
122
- # :nocov:
122
+ # simplecov:enable
123
123
  end
124
124
  end
125
125
 
@@ -155,6 +155,12 @@ module WebMock
155
155
  raise WebMock::NetConnectNotAllowedError, request_signature
156
156
  end
157
157
  end
158
+
159
+ private
160
+
161
+ def connect
162
+ super unless @mocked
163
+ end
158
164
  end
159
165
  end
160
166
 
@@ -162,18 +162,12 @@ module HTTPX
162
162
 
163
163
  return unless request
164
164
 
165
- begin
166
- request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
167
- request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
165
+ request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
166
+ request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
168
167
 
169
- response = request.response
168
+ response = request.response
170
169
 
171
- response << chunk
172
- rescue StandardError => e
173
- error_response = ErrorResponse.new(request, e)
174
- request.response = error_response
175
- dispatch(request)
176
- end
170
+ response << chunk
177
171
  end
178
172
 
179
173
  def on_complete
@@ -416,7 +410,7 @@ module HTTPX
416
410
  end
417
411
 
418
412
  def capitalized(field)
419
- UPCASED[field] || field.split("-").map(&:capitalize).join("-")
413
+ UPCASED.fetch(field) { field.split("-").map(&:capitalize).join("-") }
420
414
  end
421
415
  end
422
416
  end
@@ -37,6 +37,7 @@ module HTTPX
37
37
  @streams = {}
38
38
  @drains = {}
39
39
  @pings = []
40
+ @streams_to_close_after_receive = []
40
41
  @buffer = buffer
41
42
  @handshake_completed = false
42
43
  @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
@@ -110,6 +111,12 @@ module HTTPX
110
111
 
111
112
  def <<(data)
112
113
  @connection << data
114
+
115
+ while (stream, request, error = @streams_to_close_after_receive.shift)
116
+ # these streams were marked for cancellation due to errors found while processing the
117
+ # data received by the peer.
118
+ emit_stream_error(stream, request, error)
119
+ end
113
120
  end
114
121
 
115
122
  def send(request, head = false)
@@ -128,7 +135,7 @@ module HTTPX
128
135
  rescue ::HTTP2::Error::StreamLimitExceeded
129
136
  @pending.unshift(request)
130
137
  false
131
- rescue StandardError => e
138
+ rescue ::HTTP2::Error::Error, ArgumentError => e
132
139
  emit(:error, request, e)
133
140
  end
134
141
 
@@ -333,6 +340,8 @@ module HTTPX
333
340
  @streams[request] = stream
334
341
 
335
342
  handle(request, stream) if request.expects?
343
+ rescue HTTPX::Error => e
344
+ @streams_to_close_after_receive << [stream, request, e]
336
345
  end
337
346
 
338
347
  def on_stream_trailers(stream, request, response, h)
@@ -345,7 +354,18 @@ module HTTPX
345
354
  def on_stream_data(stream, request, data)
346
355
  request.log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
347
356
  request.log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
357
+
358
+ return unless request.response
359
+
348
360
  request.response << data
361
+ rescue HTTPX::Error => e
362
+ if stream.state == :closing
363
+ # there won't be any more chunks to process after this one.
364
+ emit_stream_error(stream, request, e)
365
+ else
366
+ # defer until the last chunk from the payload is processed.
367
+ @streams_to_close_after_receive << [stream, request, e]
368
+ end
349
369
  end
350
370
 
351
371
  def on_stream_refuse(stream, request, error)
@@ -381,11 +401,12 @@ module HTTPX
381
401
  emit(:response, request, response)
382
402
  end
383
403
  else
384
- response = request.response
385
- if response && response.is_a?(Response) && response.status == 421
386
- emit(:error, request, :http_1_1_required)
387
- else
388
- emit(:response, request, response)
404
+ if (response = request.response)
405
+ if response.is_a?(Response) && response.status == 421
406
+ emit(:error, request, :http_1_1_required)
407
+ else
408
+ emit(:response, request, response)
409
+ end
389
410
  end
390
411
  end
391
412
  send(@pending.shift) unless @pending.empty?
@@ -509,6 +530,12 @@ module HTTPX
509
530
  emit(:pong)
510
531
  end
511
532
 
533
+ def emit_stream_error(stream, request, error)
534
+ teardown(request)
535
+ stream.close
536
+ emit(:error, request, error)
537
+ end
538
+
512
539
  def teardown(request = nil)
513
540
  if request
514
541
  @drains.delete(request)
@@ -161,15 +161,17 @@ module HTTPX
161
161
  end
162
162
 
163
163
  def purge_pending(&block)
164
- pendings = []
165
164
  if @parser
166
165
  pending = @parser.pending
167
166
  @inflight -= pending.size
168
- pendings << pending
167
+ pending.reject! do |req|
168
+ block.call(req)
169
+ true
170
+ end
169
171
  end
170
- pendings << @pending
171
- pendings.each do |pending|
172
- pending.reject!(&block)
172
+ @pending.reject! do |req|
173
+ block.call(req)
174
+ true
173
175
  end
174
176
  end
175
177
 
@@ -203,7 +205,7 @@ module HTTPX
203
205
  return @parser.interests if @parser
204
206
 
205
207
  nil
206
- rescue StandardError => e
208
+ rescue Error => e
207
209
  on_error(e)
208
210
  nil
209
211
  end
@@ -215,6 +217,8 @@ module HTTPX
215
217
  def call
216
218
  case @state
217
219
  when :idle
220
+ return if no_more_requests?
221
+
218
222
  connect
219
223
 
220
224
  # when opening the tcp or ssl socket fails
@@ -222,7 +226,7 @@ module HTTPX
222
226
 
223
227
  consume
224
228
  when :closed
225
- return if @pending.empty?
229
+ return if no_more_requests?
226
230
 
227
231
  # there are pending requests to send, restart the state machine.
228
232
  idling
@@ -247,10 +251,14 @@ module HTTPX
247
251
  consume
248
252
  end
249
253
  nil
250
- rescue IOError => e
254
+ rescue Errno::ECONNRESET,
255
+ Errno::EINVAL,
256
+ SocketError,
257
+ IOError,
258
+ TLSError => e
251
259
  @write_buffer.clear
252
260
  on_io_error(e)
253
- rescue StandardError => e
261
+ rescue Error => e
254
262
  @write_buffer.clear
255
263
  on_error(e)
256
264
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -258,6 +266,10 @@ module HTTPX
258
266
  raise e
259
267
  end
260
268
 
269
+ def initial_call
270
+ call
271
+ end
272
+
261
273
  def close
262
274
  transition(:active) if @state == :inactive
263
275
 
@@ -314,6 +326,11 @@ module HTTPX
314
326
  # (example: HTTP/1 parser disabling pipelining)
315
327
  return if @state == :idle && @pending.any?
316
328
 
329
+ if @ping_timer
330
+ @ping_timer.cancel
331
+ @ping_timer = nil
332
+ end
333
+
317
334
  parser = @parser
318
335
 
319
336
  if parser && parser.respond_to?(:max_concurrent_requests)
@@ -433,6 +450,11 @@ module HTTPX
433
450
 
434
451
  def on_io_error(e)
435
452
  on_error(e)
453
+
454
+ # do not force close if parser resets the connection.
455
+ # can happen i.e. when HTTP/1.1 pipelining is disabled.
456
+ return if @state == :idle && @pending.any?
457
+
436
458
  force_close(true)
437
459
  end
438
460
 
@@ -456,7 +478,7 @@ module HTTPX
456
478
  reset
457
479
  end
458
480
 
459
- # :nocov:
481
+ # simplecov:disable
460
482
  def inspect
461
483
  "#<#{self.class}:#{object_id} " \
462
484
  "@origin=#{@origin} " \
@@ -464,7 +486,7 @@ module HTTPX
464
486
  "@pending=#{@pending.size} " \
465
487
  "@io=#{@io}>"
466
488
  end
467
- # :nocov:
489
+ # simplecov:enable
468
490
 
469
491
  private
470
492
 
@@ -490,7 +512,7 @@ module HTTPX
490
512
  # * the number of pending requests
491
513
  # * the number of inflight requests
492
514
  # * whether the write buffer has bytes (i.e. for close handshake)
493
- if @pending.empty? && @inflight.zero? && @write_buffer.empty?
515
+ if no_more_requests? && @write_buffer.empty?
494
516
  no_more_requests_loop_check if @parser && @parser.pending.any?
495
517
 
496
518
  # terminate if an altsvc connection has been established
@@ -540,7 +562,7 @@ module HTTPX
540
562
  break if @state == :closing || @state == :closed
541
563
 
542
564
  # exit #consume altogether if all outstanding requests have been dealt with
543
- if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
565
+ if no_more_requests? && @write_buffer.empty? # rubocop:disable Style/Next
544
566
  no_more_requests_loop_check if @parser && @parser.pending.any?
545
567
 
546
568
  # terminate if an altsvc connection has been established
@@ -960,6 +982,7 @@ module HTTPX
960
982
  ping_timeout = @options.timeout[:ping_timeout]
961
983
 
962
984
  @ping_timer = @current_selector.after(ping_timeout) do
985
+ log(level: 3) { "ping timeout expired..." }
963
986
  error = PingTimeoutError.new(ping_timeout, "Timed out after #{ping_timeout} seconds")
964
987
  on_error(error)
965
988
  end
@@ -986,6 +1009,11 @@ module HTTPX
986
1009
  "along with debug logs"
987
1010
  end
988
1011
 
1012
+ # true when there are no more pending nor inflight (in parser) requests
1013
+ def no_more_requests?
1014
+ @pending.empty? && @inflight.zero?
1015
+ end
1016
+
989
1017
  # recover internal state and emit all relevant error responses when +error+ was raised.
990
1018
  # this takes an optiona +request+ which may have already been handled and can be opted out
991
1019
  # in the state recovery process.
@@ -1099,13 +1127,14 @@ module HTTPX
1099
1127
  end
1100
1128
 
1101
1129
  timer = selector.after(timeout, callback)
1102
- request.active_timeouts << label
1130
+ timer.label = label
1131
+ request.active_timeouts << timer
1103
1132
 
1104
1133
  Array(finish_events).each do |event|
1105
1134
  # clean up request timeouts if the connection errors out
1106
1135
  request.set_timeout_callback(event) do
1107
1136
  timer.cancel
1108
- request.active_timeouts.delete(label)
1137
+ request.active_timeouts.delete(timer)
1109
1138
  end
1110
1139
  end
1111
1140
  end
data/lib/httpx/headers.rb CHANGED
@@ -140,12 +140,12 @@ module HTTPX
140
140
  @headers.to_s
141
141
  end
142
142
 
143
- # :nocov:
143
+ # simplecov:disable
144
144
  def inspect
145
145
  "#<#{self.class}:#{object_id} " \
146
146
  "#{to_hash.inspect}>"
147
147
  end
148
- # :nocov:
148
+ # simplecov:enable
149
149
 
150
150
  # this is internal API and doesn't abide to other public API
151
151
  # guarantees, like downcasing strings.
data/lib/httpx/io/ssl.rb CHANGED
@@ -20,8 +20,9 @@ module HTTPX
20
20
  super
21
21
 
22
22
  @ssl_session = nil
23
- ctx_options = TLS_OPTIONS.merge(options.ssl)
24
- @sni_hostname = ctx_options.delete(:hostname) || @hostname
23
+ ctx_options = TLS_OPTIONS
24
+ ctx_options = ctx_options.merge(options.ssl) if options.ssl && !options.ssl.empty?
25
+ @sni_hostname = (ctx_options.delete(:hostname) if ctx_options.key?(:hostname)) || @hostname
25
26
 
26
27
  if @keep_open && @io.is_a?(OpenSSL::SSL::SSLSocket)
27
28
  # externally initiated ssl socket
@@ -29,7 +30,7 @@ module HTTPX
29
30
  @state = :negotiated
30
31
  else
31
32
  @ctx = OpenSSL::SSL::SSLContext.new
32
- @ctx.set_params(ctx_options) unless ctx_options.empty?
33
+ @ctx.set_params(ctx_options)
33
34
  unless @ctx.session_cache_mode.nil? # a dummy method on JRuby
34
35
  @ctx.session_cache_mode =
35
36
  OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
@@ -51,24 +52,22 @@ module HTTPX
51
52
  end
52
53
 
53
54
  def protocol
54
- # @type ivar @io: OpenSSL::SSL::SSLSocket
55
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
56
+
55
57
  @io.alpn_protocol || super
56
- rescue StandardError
57
- super
58
58
  end
59
59
 
60
60
  if RUBY_ENGINE == "jruby"
61
61
  # in jruby, alpn_protocol may return ""
62
62
  # https://github.com/jruby/jruby-openssl/issues/287
63
63
  def protocol
64
- # @type ivar @io: OpenSSL::SSL::SSLSocket
64
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
65
+
65
66
  proto = @io.alpn_protocol
66
67
 
67
68
  return super if proto.nil? || proto.empty?
68
69
 
69
70
  proto
70
- rescue StandardError
71
- super
72
71
  end
73
72
  end
74
73
 
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
@@ -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)
@@ -127,8 +127,8 @@ module HTTPX
127
127
  @auth_header_expires_at = if (expires_in = request.options.auth_header_expires_in)
128
128
  Time.now.to_i + expires_in
129
129
  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
130
+ if expires_at.respond_to?(:call) && (expires_at = expires_at.call(request))
131
+ expires_at = expires_at.to_f
132
132
  raise Error, "`:auth_header_expires_at` must be positive" unless expires_at.positive?
133
133
 
134
134
  expires_at
@@ -204,6 +204,15 @@ module HTTPX
204
204
  request.unauthorize!
205
205
  end
206
206
 
207
+ def when_to_retry(request, response, *)
208
+ return super unless response.is_a?(Response)
209
+
210
+ # retry immediately if authentication no longer valid.
211
+ return if response.status == 401
212
+
213
+ super
214
+ end
215
+
207
216
  def auth_error?(response, options)
208
217
  response.is_a?(Response) && response.status == 401 && dynamic_auth_token?(options.auth_header_value)
209
218
  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
@@ -55,13 +55,29 @@ module HTTPX
55
55
 
56
56
  private
57
57
 
58
+ def reconnectable_error?(error)
59
+ Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
60
+ end
61
+
62
+ def when_to_retry(request, response, *)
63
+ return super unless response.is_a?(ErrorResponse)
64
+
65
+ error = response.error
66
+ # allow request to be retried immediately if the request failed right after the keep alive timeout.
67
+ # the chances are, the request failed because the connect has been dropped by the peer server, so it's
68
+ # fine to reopen.
69
+ return if request.ping? && reconnectable_error?(error)
70
+
71
+ super
72
+ end
73
+
58
74
  def retryable_request?(request, response, *)
59
75
  super || begin
60
- return false unless response && response.is_a?(ErrorResponse)
76
+ return false unless response.is_a?(ErrorResponse)
61
77
 
62
78
  error = response.error
63
79
 
64
- Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
80
+ reconnectable_error?(error)
65
81
  end
66
82
  end
67
83
 
@@ -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"]
@@ -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
 
data/lib/httpx/pool.rb CHANGED
@@ -173,7 +173,7 @@ module HTTPX
173
173
  end
174
174
  end
175
175
 
176
- # :nocov:
176
+ # simplecov:disable
177
177
  def inspect
178
178
  "#<#{self.class}:#{object_id} " \
179
179
  "@max_connections=#{@max_connections} " \
@@ -181,7 +181,7 @@ module HTTPX
181
181
  "@pool_timeout=#{@pool_timeout} " \
182
182
  "@connections=#{@connections.size}>"
183
183
  end
184
- # :nocov:
184
+ # simplecov:enable
185
185
 
186
186
  private
187
187
 
@@ -114,12 +114,12 @@ module HTTPX
114
114
  @headers.add("transfer-encoding", "chunked")
115
115
  end
116
116
 
117
- # :nocov:
117
+ # simplecov:disable
118
118
  def inspect
119
119
  "#<#{self.class}:#{object_id} " \
120
120
  "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
121
121
  end
122
- # :nocov:
122
+ # simplecov:enable
123
123
 
124
124
  class << self
125
125
  def initialize_body(params)
data/lib/httpx/request.rb CHANGED
@@ -122,6 +122,7 @@ module HTTPX
122
122
 
123
123
  def complete!(response = @response)
124
124
  emit(:complete, response)
125
+ reset_timers(true)
125
126
  end
126
127
 
127
128
  # whether request has been buffered with a ping
@@ -268,21 +269,19 @@ module HTTPX
268
269
 
269
270
  # consumes and returns the next available chunk of request body that can be sent
270
271
  def drain_body
271
- return nil if @body.nil?
272
+ return if @body.nil?
272
273
 
273
274
  @drainer ||= @body.each
274
- chunk = @drainer.next.dup
275
-
276
- emit(:body_chunk, chunk)
277
- chunk
275
+ @drainer.next.dup
278
276
  rescue StopIteration
279
277
  nil
280
278
  rescue StandardError => e
279
+ # in case an error occurs while emitting body chunks
281
280
  @drain_error = e
282
281
  nil
283
282
  end
284
283
 
285
- # :nocov:
284
+ # simplecov:disable
286
285
  def inspect
287
286
  "#<#{self.class}:#{object_id} " \
288
287
  "#{@verb} " \
@@ -290,7 +289,7 @@ module HTTPX
290
289
  "@headers=#{@headers} " \
291
290
  "@body=#{@body}>"
292
291
  end
293
- # :nocov:
292
+ # simplecov:enable
294
293
 
295
294
  # moves on to the +nextstate+ of the request state machine (when all preconditions are met)
296
295
  def transition(nextstate)
@@ -299,7 +298,10 @@ module HTTPX
299
298
  @body.rewind
300
299
  @ping = false
301
300
  @response = @drainer = nil
302
- @active_timeouts.clear
301
+
302
+ # request may be sent to a different connection and will be
303
+ # reassigned a new set of timers.
304
+ reset_timers(false)
303
305
  when :headers
304
306
  return unless @state == :idle
305
307
 
@@ -326,7 +328,7 @@ module HTTPX
326
328
  return if @state == :expect
327
329
 
328
330
  end
329
- log(level: 3) { "#{@state}] -> #{nextstate}" }
331
+ log(level: 3) { "#{@state} -> #{nextstate}" }
330
332
  @state = nextstate
331
333
  emit(@state, self)
332
334
  nil
@@ -363,6 +365,19 @@ module HTTPX
363
365
 
364
366
  @on_response_arrived.call
365
367
  end
368
+
369
+ private
370
+
371
+ def reset_timers(reset_total_request_timers)
372
+ timers = @active_timeouts
373
+
374
+ until (timer = timers.shift).nil?
375
+ next if !reset_total_request_timers && timer.label == :total_request_timeout
376
+
377
+ # cancel active timers.
378
+ timer.cancel
379
+ end
380
+ end
366
381
  end
367
382
  end
368
383
 
@@ -106,14 +106,7 @@ module HTTPX
106
106
  name = entry["name"]
107
107
  next unless name != hostname
108
108
 
109
- lookups[name] ||= []
110
-
111
- case family
112
- when Socket::AF_INET6
113
- lookups[name] << entry
114
- when Socket::AF_INET
115
- lookups[name].unshift(entry)
116
- end
109
+ _set(name, family, [entry], lookups, hostnames)
117
110
  end
118
111
  end
119
112
 
@@ -126,27 +126,27 @@ module HTTPX
126
126
  end
127
127
 
128
128
  def on_response(request, response)
129
- response.raise_for_status
130
- rescue StandardError => e
131
- hostname = @requests.delete(request)
132
- connection = reset_hostname(hostname)
133
- emit_resolve_error(connection, connection.peer.host, e)
134
- close_or_resolve
135
- else
136
- # @type var response: HTTPX::Response
137
- if response.status.between?(300, 399) && response.headers.key?("location")
138
- hostname = @requests[request]
139
- connection = @queries[hostname]
140
- location_uri = URI(response.headers["location"])
141
- location_uri = response.uri.merge(location_uri) if location_uri.relative?
142
-
143
- # we assume that the DNS server URI changed permanently and move on
144
- @uri = location_uri
145
- send_request(hostname, connection)
146
- return
147
- end
129
+ if (e = response.error)
130
+ hostname = @requests.delete(request)
131
+ connection = reset_hostname(hostname)
132
+ emit_resolve_error(connection, connection.peer.host, e)
133
+ close_or_resolve
134
+ else
135
+ # @type var response: HTTPX::Response
136
+ if response.status.between?(300, 399) && response.headers.key?("location")
137
+ hostname = @requests[request]
138
+ connection = @queries[hostname]
139
+ location_uri = URI(response.headers["location"])
140
+ location_uri = response.uri.merge(location_uri) if location_uri.relative?
148
141
 
149
- parse(request, response)
142
+ # we assume that the DNS server URI changed permanently and move on
143
+ @uri = location_uri
144
+ send_request(hostname, connection)
145
+ return
146
+ end
147
+
148
+ parse(request, response)
149
+ end
150
150
  ensure
151
151
  @requests.delete(request)
152
152
  end
@@ -75,6 +75,13 @@ module HTTPX
75
75
 
76
76
  def call
77
77
  case @state
78
+ when :idle, :closed
79
+ return if @connections.empty?
80
+
81
+ transition(:idle) if @state == :closed
82
+ transition(:open)
83
+
84
+ consume if @state == :open
78
85
  when :open
79
86
  consume
80
87
  end
@@ -52,6 +52,10 @@ module HTTPX
52
52
 
53
53
  alias_method :terminate, :close
54
54
 
55
+ def initial_call
56
+ call
57
+ end
58
+
55
59
  def force_close(*args)
56
60
  while (connection = @connections.shift)
57
61
  connection.force_close(*args)
@@ -23,6 +23,9 @@ module HTTPX
23
23
  DONE = 1
24
24
  ERROR = 2
25
25
 
26
+ class AddrinfoTimeoutError < StandardError
27
+ end
28
+
26
29
  class << self
27
30
  def multi?
28
31
  false
@@ -227,7 +230,7 @@ module HTTPX
227
230
  begin
228
231
  addrs = if resolve_timeout
229
232
 
230
- Timeout.timeout(resolve_timeout) do
233
+ Timeout.timeout(resolve_timeout, AddrinfoTimeoutError) do
231
234
  __addrinfo_resolve(hostname, scheme)
232
235
  end
233
236
  else
@@ -246,7 +249,7 @@ module HTTPX
246
249
  end
247
250
  end
248
251
  rescue StandardError => e
249
- if e.is_a?(Timeout::Error)
252
+ if e.is_a?(AddrinfoTimeoutError)
250
253
  timeouts.shift
251
254
  retry unless timeouts.empty?
252
255
  e = ResolveTimeoutError.new(resolve_timeout, e.message)
@@ -161,13 +161,13 @@ module HTTPX
161
161
  end
162
162
  end
163
163
 
164
- # :nocov:
164
+ # simplecov:disable
165
165
  def inspect
166
166
  "#<#{self.class}:#{object_id} " \
167
167
  "@state=#{@state} " \
168
168
  "@length=#{@length}>"
169
169
  end
170
- # :nocov:
170
+ # simplecov:enable
171
171
 
172
172
  # rewinds the response payload buffer.
173
173
  def rewind
@@ -141,7 +141,7 @@ module HTTPX
141
141
  bodyless? || (@request.verb == "CONNECT" && @status == 200)
142
142
  end
143
143
 
144
- # :nocov:
144
+ # simplecov:disable
145
145
  def inspect
146
146
  "#<#{self.class}:#{object_id} " \
147
147
  "HTTP/#{version} " \
@@ -149,7 +149,7 @@ module HTTPX
149
149
  "@headers=#{@headers} " \
150
150
  "@body=#{@body.bytesize}>"
151
151
  end
152
- # :nocov:
152
+ # simplecov:enable
153
153
 
154
154
  # returns an instance of HTTPX::HTTPError if the response has a 4xx or 5xx
155
155
  # status code, or nothing.
@@ -267,7 +267,6 @@ module HTTPX
267
267
  # response = HTTPX.get("https://some-domain/path") #=> response is HTTPX::Response or HTTPX::ErrorResponse
268
268
  # response.raise_for_status #=> raises if it wraps an error
269
269
  class ErrorResponse
270
- include Loggable
271
270
  extend Forwardable
272
271
 
273
272
  # the corresponding HTTPX::Request instance.
@@ -290,7 +289,7 @@ module HTTPX
290
289
  @response = request.response if request.response.is_a?(Response)
291
290
  @error = error
292
291
  @options = request.options
293
- log_exception(@error)
292
+ @request.log_exception(@error)
294
293
  finish!
295
294
  end
296
295
 
@@ -41,6 +41,11 @@ module HTTPX
41
41
  @selectables.empty? && @timers.empty?
42
42
  end
43
43
 
44
+ # first time the registered selectables are added, there's probably work to do.
45
+ def initial_call
46
+ @selectables.each(&:initial_call)
47
+ end
48
+
44
49
  def next_tick
45
50
  catch(:jump_tick) do
46
51
  timeout = next_timeout
data/lib/httpx/session.rb CHANGED
@@ -319,9 +319,14 @@ module HTTPX
319
319
 
320
320
  waiting = false
321
321
 
322
- responses = requests.each_with_index.map do |request, idx|
322
+ requests.each do |request|
323
323
  send_request(request, selector)
324
+ end
324
325
 
326
+ # do work first
327
+ selector.initial_call
328
+
329
+ responses = requests.each_with_index.map do |request, idx|
325
330
  fetch_response(request, selector, request.options).tap do |response|
326
331
  if response.nil?
327
332
  pending += 1
@@ -20,11 +20,11 @@ module HTTPX
20
20
  $VERBOSE = original_verbosity
21
21
  end
22
22
 
23
- # :nocov:
23
+ # simplecov:disable
24
24
  if Session.default_options.debug_level > 2
25
25
  proxy_session = plugin(:internal_telemetry)
26
26
  remove_const(:Session)
27
27
  const_set(:Session, proxy_session.class)
28
28
  end
29
- # :nocov:
29
+ # simplecov:enable
30
30
  end
data/lib/httpx/timers.rb CHANGED
@@ -62,6 +62,9 @@ module HTTPX
62
62
  end
63
63
 
64
64
  class Timer
65
+ # simpler helper which allows classification
66
+ attr_accessor :label
67
+
65
68
  def initialize(interval, callback)
66
69
  @interval = interval
67
70
  @callback = callback
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.0"
4
+ VERSION = "1.8.1"
5
5
  end
@@ -16,6 +16,7 @@ module HTTPX
16
16
  @max_requests: Integer
17
17
  @drains: Hash[Request, String]
18
18
  @pings: Array[String]
19
+ @streams_to_close_after_receive: Array[[HTTP2::Stream, Request, HTTPX::Error]]
19
20
  @buffer: Buffer
20
21
  @handshake_completed: bool
21
22
  @wait_for_handshake: bool
@@ -106,6 +107,8 @@ module HTTPX
106
107
 
107
108
  def on_pong: (string ping) -> void
108
109
 
110
+ def emit_stream_error: (HTTP2::Stream stream, Request request, StandardError error) -> void
111
+
109
112
  def teardown: (?Request? request) -> void
110
113
 
111
114
  class Error < ::HTTPX::Error
data/sig/connection.rbs CHANGED
@@ -92,6 +92,8 @@ module HTTPX
92
92
 
93
93
  def call: () -> void
94
94
 
95
+ def initial_call: () -> void
96
+
95
97
  def terminate: () -> void
96
98
 
97
99
  def close: () -> void
@@ -122,7 +124,7 @@ module HTTPX
122
124
 
123
125
  def on_error: (HTTPX::TimeoutError | Error | StandardError error, ?Request? request) -> void
124
126
 
125
- def on_io_error: (IOError error) -> void
127
+ def on_io_error: (SocketError | IOError | TLSError error) -> void
126
128
 
127
129
  def on_connect_error: (Exception error) -> void
128
130
 
@@ -164,6 +166,8 @@ module HTTPX
164
166
 
165
167
  def no_more_requests_loop_check: () -> void
166
168
 
169
+ def no_more_requests?: () -> bool
170
+
167
171
  def handle_error: (StandardError error, ?Request? request) -> void
168
172
 
169
173
  def force_purge: () -> void
data/sig/loggable.rbs CHANGED
@@ -12,10 +12,14 @@ module HTTPX
12
12
 
13
13
  COLORS: Hash[Symbol, Integer]
14
14
 
15
+ def self.log_identifiers: () -> String
16
+
15
17
  def log: (?level: Integer?, ?color: Symbol?, ?debug_level: Integer, ?debug: _IOLogger?) { () -> String } -> void
16
18
 
17
19
  def log_exception: (Exception error, ?level: Integer, ?color: Symbol, ?debug_level: Integer, ?debug: _IOLogger?) -> void
18
20
 
21
+ private
22
+
19
23
  def log_redact_headers: (_ToS text) -> String
20
24
 
21
25
  def log_redact_body: (_ToS text) -> String
data/sig/plugins/auth.rbs CHANGED
@@ -7,6 +7,10 @@ module HTTPX
7
7
  def auth_header_value: () -> auth_header_value_type?
8
8
 
9
9
  def auth_header_type: () -> String?
10
+
11
+ def auth_header_expires_at: () -> (^(Request request) -> _ToF?)?
12
+
13
+ def auth_header_expires_in: () -> Float?
10
14
  end
11
15
 
12
16
  module InstanceMethods
@@ -6,6 +6,9 @@ module HTTPX
6
6
  def self.extra_options: (Options) -> (Options)
7
7
 
8
8
  module InstanceMethods
9
+ private
10
+
11
+ def reconnectable_error?: (StandardError error) -> bool
9
12
  end
10
13
  end
11
14
 
data/sig/request.rbs CHANGED
@@ -17,7 +17,7 @@ module HTTPX
17
17
  attr_reader response: response?
18
18
  attr_reader http2_stream_options: { ?dependency: Integer, ?exclusive: bool, ?weight: Integer }
19
19
  attr_reader drain_error: StandardError?
20
- attr_reader active_timeouts: Array[Symbol]
20
+ attr_reader active_timeouts: Array[Timers::Timer]
21
21
 
22
22
  attr_writer connection: Connection?
23
23
  attr_writer on_response_arrived: (^() -> void)?
@@ -93,7 +93,6 @@ module HTTPX
93
93
 
94
94
  private
95
95
 
96
- def initialize_body: (Options options) -> Transcoder::_Encoder?
97
-
96
+ def reset_timers: (bool reset_total_request_timers) -> void
98
97
  end
99
98
  end
@@ -5,6 +5,9 @@ module HTTPX
5
5
  DONE: 1
6
6
  ERROR: 2
7
7
 
8
+ class AddrinfoTimeoutError < StandardError
9
+ end
10
+
8
11
  @resolver: Resolv::DNS
9
12
  @_timeouts: Array[interval]
10
13
  @timeouts: Hash[String, Array[interval]]
data/sig/selector.rbs CHANGED
@@ -6,6 +6,8 @@ module HTTPX
6
6
 
7
7
  def call: () -> void
8
8
 
9
+ def initial_call: () -> void
10
+
9
11
  def interests: () -> io_interests?
10
12
 
11
13
  def timeout: () -> interval?
data/sig/timers.rbs CHANGED
@@ -48,6 +48,8 @@ module HTTPX
48
48
  @interval: Interval
49
49
  @callback: callback
50
50
 
51
+ attr_accessor label: Symbol?
52
+
51
53
  def initialize: (Interval interval, callback callback) -> void
52
54
 
53
55
  def cancel: () -> void
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.0
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -169,6 +169,7 @@ extra_rdoc_files:
169
169
  - doc/release_notes/1_7_7.md
170
170
  - doc/release_notes/1_7_8.md
171
171
  - doc/release_notes/1_8_0.md
172
+ - doc/release_notes/1_8_1.md
172
173
  files:
173
174
  - LICENSE.txt
174
175
  - README.md
@@ -310,6 +311,7 @@ files:
310
311
  - doc/release_notes/1_7_7.md
311
312
  - doc/release_notes/1_7_8.md
312
313
  - doc/release_notes/1_8_0.md
314
+ - doc/release_notes/1_8_1.md
313
315
  - lib/httpx.rb
314
316
  - lib/httpx/adapters/datadog.rb
315
317
  - lib/httpx/adapters/faraday.rb