httpx 1.4.0 → 1.4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/doc/release_notes/1_4_2.md +20 -0
  5. data/lib/httpx/adapters/datadog.rb +55 -83
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/webmock.rb +7 -1
  8. data/lib/httpx/callbacks.rb +2 -2
  9. data/lib/httpx/connection/http2.rb +2 -2
  10. data/lib/httpx/connection.rb +106 -51
  11. data/lib/httpx/errors.rb +3 -0
  12. data/lib/httpx/loggable.rb +8 -1
  13. data/lib/httpx/plugins/callbacks.rb +1 -0
  14. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  15. data/lib/httpx/plugins/expect.rb +1 -1
  16. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  17. data/lib/httpx/request/body.rb +9 -14
  18. data/lib/httpx/request.rb +20 -0
  19. data/lib/httpx/resolver/https.rb +4 -2
  20. data/lib/httpx/resolver/native.rb +111 -55
  21. data/lib/httpx/resolver/resolver.rb +18 -11
  22. data/lib/httpx/resolver/system.rb +3 -5
  23. data/lib/httpx/selector.rb +33 -23
  24. data/lib/httpx/session.rb +17 -43
  25. data/lib/httpx/timers.rb +16 -1
  26. data/lib/httpx/transcoder/body.rb +15 -31
  27. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  28. data/lib/httpx/version.rb +1 -1
  29. data/lib/httpx.rb +1 -1
  30. data/sig/callbacks.rbs +2 -2
  31. data/sig/connection.rbs +19 -5
  32. data/sig/errors.rbs +3 -0
  33. data/sig/request/body.rbs +0 -8
  34. data/sig/request.rbs +3 -0
  35. data/sig/resolver/native.rbs +6 -1
  36. data/sig/selector.rbs +1 -0
  37. data/sig/session.rbs +2 -0
  38. data/sig/timers.rbs +15 -4
  39. data/sig/transcoder/body.rbs +1 -3
  40. data/sig/transcoder/utils/body_reader.rbs +1 -1
  41. data/sig/transcoder/utils/deflater.rbs +1 -1
  42. metadata +7 -3
@@ -41,15 +41,17 @@ module HTTPX
41
41
 
42
42
  def_delegator :@write_buffer, :empty?
43
43
 
44
- attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session
44
+ attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling
45
45
 
46
- attr_writer :current_selector, :coalesced_connection
46
+ attr_writer :current_selector
47
47
 
48
48
  attr_accessor :current_session, :family
49
49
 
50
+ protected :sibling
51
+
50
52
  def initialize(uri, options)
51
- @current_session = @current_selector = @coalesced_connection = nil
52
- @exhausted = @cloned = false
53
+ @current_session = @current_selector = @sibling = @coalesced_connection = nil
54
+ @exhausted = @cloned = @main_sibling = false
53
55
 
54
56
  @options = Options.new(options)
55
57
  @type = initialize_type(uri, @options)
@@ -70,9 +72,6 @@ module HTTPX
70
72
  else
71
73
  transition(:idle)
72
74
  end
73
- on(:activate) do
74
- @current_session.select_connection(self, @current_selector)
75
- end
76
75
  on(:close) do
77
76
  next if @exhausted # it'll reset
78
77
 
@@ -86,10 +85,13 @@ module HTTPX
86
85
  on(:terminate) do
87
86
  next if @exhausted # it'll reset
88
87
 
88
+ current_session = @current_session
89
+ current_selector = @current_selector
90
+
89
91
  # may be called after ":close" above, so after the connection has been checked back in.
90
- next unless @current_session
92
+ next unless current_session && current_selector
91
93
 
92
- @current_session.deselect_connection(self, @current_selector)
94
+ current_session.deselect_connection(self, current_selector)
93
95
  end
94
96
 
95
97
  on(:altsvc) do |alt_origin, origin, alt_params|
@@ -99,8 +101,6 @@ module HTTPX
99
101
  @inflight = 0
100
102
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
101
103
 
102
- @intervals = []
103
-
104
104
  self.addresses = @options.addresses if @options.addresses
105
105
  end
106
106
 
@@ -194,6 +194,12 @@ module HTTPX
194
194
  end
195
195
  end
196
196
 
197
+ def io_connected?
198
+ return @coalesced_connection.io_connected? if @coalesced_connection
199
+
200
+ @io && @io.state == :connected
201
+ end
202
+
197
203
  def connecting?
198
204
  @state == :idle
199
205
  end
@@ -329,31 +335,54 @@ module HTTPX
329
335
  end
330
336
 
331
337
  def handle_socket_timeout(interval)
332
- @intervals.delete_if(&:elapsed?)
338
+ error = OperationTimeoutError.new(interval, "timed out while waiting on select")
339
+ error.set_backtrace(caller)
340
+ on_error(error)
341
+ end
333
342
 
334
- unless @intervals.empty?
335
- # remove the intervals which will elapse
343
+ def coalesced_connection=(connection)
344
+ @coalesced_connection = connection
336
345
 
337
- return
338
- end
346
+ close_sibling
347
+ connection.merge(self)
348
+ end
339
349
 
340
- error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
341
- error.set_backtrace(caller)
342
- on_error(error)
350
+ def sibling=(connection)
351
+ @sibling = connection
352
+
353
+ return unless connection
354
+
355
+ @main_sibling = connection.sibling.nil?
356
+
357
+ return unless @main_sibling
358
+
359
+ connection.sibling = self
343
360
  end
344
361
 
345
- private
362
+ def handle_connect_error(error)
363
+ @connect_error = error
346
364
 
347
- def connect
348
- transition(:open)
365
+ return handle_error(error) unless @sibling && @sibling.connecting?
366
+
367
+ @sibling.merge(self)
368
+
369
+ force_reset(true)
349
370
  end
350
371
 
351
372
  def disconnect
373
+ return unless @current_session && @current_selector
374
+
352
375
  emit(:close)
353
376
  @current_session = nil
354
377
  @current_selector = nil
355
378
  end
356
379
 
380
+ private
381
+
382
+ def connect
383
+ transition(:open)
384
+ end
385
+
357
386
  def consume
358
387
  return unless @io
359
388
 
@@ -448,6 +477,8 @@ module HTTPX
448
477
  end
449
478
  log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
450
479
  unless siz
480
+ @write_buffer.clear
481
+
451
482
  ex = EOFError.new("descriptor closed")
452
483
  ex.set_backtrace(caller)
453
484
  on_error(ex)
@@ -531,20 +562,22 @@ module HTTPX
531
562
  @exhausted = true
532
563
  current_session = @current_session
533
564
  current_selector = @current_selector
534
- parser.close
535
- @pending.concat(parser.pending)
565
+ begin
566
+ parser.close
567
+ @pending.concat(parser.pending)
568
+ ensure
569
+ @current_session = current_session
570
+ @current_selector = current_selector
571
+ end
572
+
536
573
  case @state
537
574
  when :closed
538
575
  idling
539
576
  @exhausted = false
540
- @current_session = current_session
541
- @current_selector = current_selector
542
577
  when :closing
543
- once(:close) do
578
+ once(:closed) do
544
579
  idling
545
580
  @exhausted = false
546
- @current_session = current_session
547
- @current_selector = current_selector
548
581
  end
549
582
  end
550
583
  end
@@ -589,11 +622,15 @@ module HTTPX
589
622
  other_connection.merge(self)
590
623
  request.transition(:idle)
591
624
  other_connection.send(request)
592
- else
593
- response = ErrorResponse.new(request, ex)
594
- request.response = response
595
- request.emit(:response, response)
625
+ next
626
+ when OperationTimeoutError
627
+ # request level timeouts should take precedence
628
+ next unless request.active_timeouts.empty?
596
629
  end
630
+
631
+ response = ErrorResponse.new(request, ex)
632
+ request.response = response
633
+ request.emit(:response, response)
597
634
  end
598
635
  end
599
636
 
@@ -613,14 +650,16 @@ module HTTPX
613
650
  # connect errors, exit gracefully
614
651
  error = ConnectionError.new(e.message)
615
652
  error.set_backtrace(e.backtrace)
616
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
653
+ handle_connect_error(error) if connecting?
617
654
  @state = :closed
655
+ purge_after_closed
618
656
  disconnect
619
657
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
620
658
  # connect errors, exit gracefully
621
659
  handle_error(e)
622
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
660
+ handle_connect_error(e) if connecting?
623
661
  @state = :closed
662
+ purge_after_closed
624
663
  disconnect
625
664
  end
626
665
 
@@ -634,7 +673,7 @@ module HTTPX
634
673
  return if @state == :closed
635
674
 
636
675
  @io.connect
637
- emit(:tcp_open, self) if @io.state == :connected
676
+ close_sibling if @io.state == :connected
638
677
 
639
678
  return unless @io.connected?
640
679
 
@@ -667,6 +706,7 @@ module HTTPX
667
706
 
668
707
  purge_after_closed
669
708
  disconnect if @pending.empty?
709
+
670
710
  when :already_open
671
711
  nextstate = :open
672
712
  # the first check for given io readiness must still use a timeout.
@@ -677,11 +717,29 @@ module HTTPX
677
717
  return unless @state == :inactive
678
718
 
679
719
  nextstate = :open
680
- emit(:activate)
720
+
721
+ # activate
722
+ @current_session.select_connection(self, @current_selector)
681
723
  end
682
724
  @state = nextstate
683
725
  end
684
726
 
727
+ def close_sibling
728
+ return unless @sibling
729
+
730
+ if @sibling.io_connected?
731
+ reset
732
+ # TODO: transition connection to closed
733
+ end
734
+
735
+ unless @sibling.state == :closed
736
+ merge(@sibling) unless @main_sibling
737
+ @sibling.force_reset(true)
738
+ end
739
+
740
+ @sibling = nil
741
+ end
742
+
685
743
  def purge_after_closed
686
744
  @io.close if @io
687
745
  @read_buffer.clear
@@ -754,7 +812,7 @@ module HTTPX
754
812
  end
755
813
 
756
814
  def on_error(error, request = nil)
757
- if error.instance_of?(TimeoutError)
815
+ if error.is_a?(OperationTimeoutError)
758
816
 
759
817
  # inactive connections do not contribute to the select loop, therefore
760
818
  # they should not fail due to such errors.
@@ -799,7 +857,7 @@ module HTTPX
799
857
 
800
858
  return if read_timeout.nil? || read_timeout.infinite?
801
859
 
802
- set_request_timeout(request, read_timeout, :done, :response) do
860
+ set_request_timeout(:read_timeout, request, read_timeout, :done, :response) do
803
861
  read_timeout_callback(request, read_timeout)
804
862
  end
805
863
  end
@@ -809,7 +867,7 @@ module HTTPX
809
867
 
810
868
  return if write_timeout.nil? || write_timeout.infinite?
811
869
 
812
- set_request_timeout(request, write_timeout, :headers, %i[done response]) do
870
+ set_request_timeout(:write_timeout, request, write_timeout, :headers, %i[done response]) do
813
871
  write_timeout_callback(request, write_timeout)
814
872
  end
815
873
  end
@@ -819,7 +877,7 @@ module HTTPX
819
877
 
820
878
  return if request_timeout.nil? || request_timeout.infinite?
821
879
 
822
- set_request_timeout(request, request_timeout, :headers, :complete) do
880
+ set_request_timeout(:request_timeout, request, request_timeout, :headers, :complete) do
823
881
  read_timeout_callback(request, request_timeout, RequestTimeoutError)
824
882
  end
825
883
  end
@@ -844,21 +902,18 @@ module HTTPX
844
902
  on_error(error, request)
845
903
  end
846
904
 
847
- def set_request_timeout(request, timeout, start_event, finish_events, &callback)
848
- request.once(start_event) do
849
- interval = @current_selector.after(timeout, callback)
905
+ def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
906
+ request.set_timeout_callback(start_event) do
907
+ timer = @current_selector.after(timeout, callback)
908
+ request.active_timeouts << label
850
909
 
851
910
  Array(finish_events).each do |event|
852
911
  # clean up request timeouts if the connection errors out
853
- request.once(event) do
854
- if @intervals.include?(interval)
855
- interval.delete(callback)
856
- @intervals.delete(interval) if interval.no_callbacks?
857
- end
912
+ request.set_timeout_callback(event) do
913
+ timer.cancel
914
+ request.active_timeouts.delete(label)
858
915
  end
859
916
  end
860
-
861
- @intervals << interval
862
917
  end
863
918
  end
864
919
 
data/lib/httpx/errors.rb CHANGED
@@ -77,6 +77,9 @@ module HTTPX
77
77
  # Error raised when there was a timeout while resolving a domain to an IP.
78
78
  class ResolveTimeoutError < TimeoutError; end
79
79
 
80
+ # Error raise when there was a timeout waiting for readiness of the socket the request is related to.
81
+ class OperationTimeoutError < TimeoutError; end
82
+
80
83
  # Error raised when there was an error while resolving a domain to an IP.
81
84
  class ResolveError < Error; end
82
85
 
@@ -22,7 +22,14 @@ module HTTPX
22
22
 
23
23
  return unless debug_stream
24
24
 
25
- message = (+"" << msg.call << "\n")
25
+ klass = self.class
26
+
27
+ until (class_name = klass.name)
28
+ klass = klass.superclass
29
+ end
30
+
31
+ message = +"(pid:#{Process.pid} tid:#{Thread.current.object_id}, self:#{class_name}##{object_id}) "
32
+ message << msg.call << "\n"
26
33
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
27
34
  debug_stream << message
28
35
  end
@@ -25,6 +25,7 @@ module HTTPX
25
25
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
26
26
  def on_#{meth}(&blk) # def on_connection_opened(&blk)
27
27
  on(:#{meth}, &blk) # on(:connection_opened, &blk)
28
+ self # self
28
29
  end # end
29
30
  MOD
30
31
  end
@@ -36,6 +36,7 @@ module HTTPX
36
36
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
37
37
  def on_#{meth}(&blk) # def on_circuit_open(&blk)
38
38
  on(:#{meth}, &blk) # on(:circuit_open, &blk)
39
+ self # self
39
40
  end # end
40
41
  MOD
41
42
  end
@@ -84,7 +84,7 @@ module HTTPX
84
84
 
85
85
  return if expect_timeout.nil? || expect_timeout.infinite?
86
86
 
87
- set_request_timeout(request, expect_timeout, :expect, %i[body response]) do
87
+ set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
88
88
  # expect timeout expired
89
89
  if request.state == :expect && !request.expects?
90
90
  Expect.no_expect_store << request.origin
@@ -29,6 +29,8 @@ module HTTPX
29
29
 
30
30
  buf = outbuf if outbuf
31
31
 
32
+ buf = buf.b if buf.frozen?
33
+
32
34
  buf.prepend([compressed_flag, buf.bytesize].pack("CL>"))
33
35
  buf
34
36
  end
@@ -52,7 +52,11 @@ module HTTPX
52
52
 
53
53
  body = stream(@body)
54
54
  if body.respond_to?(:read)
55
- ::IO.copy_stream(body, ProcIO.new(block))
55
+ while (chunk = body.read(16_384))
56
+ block.call(chunk)
57
+ end
58
+ # TODO: use copy_stream once bug is resolved: https://bugs.ruby-lang.org/issues/21131
59
+ # ::IO.copy_stream(body, ProcIO.new(block))
56
60
  elsif body.respond_to?(:each)
57
61
  body.each(&block)
58
62
  else
@@ -60,6 +64,10 @@ module HTTPX
60
64
  end
61
65
  end
62
66
 
67
+ def close
68
+ @body.close if @body.respond_to?(:close)
69
+ end
70
+
63
71
  # if the +@body+ is rewindable, it rewinnds it.
64
72
  def rewind
65
73
  return if empty?
@@ -142,17 +150,4 @@ module HTTPX
142
150
  end
143
151
  end
144
152
  end
145
-
146
- # Wrapper yielder which can be used with functions which expect an IO writer.
147
- class ProcIO
148
- def initialize(block)
149
- @block = block
150
- end
151
-
152
- # Implementation the IO write protocol, which yield the given chunk to +@block+.
153
- def write(data)
154
- @block.call(data.dup)
155
- data.bytesize
156
- end
157
- end
158
153
  end
data/lib/httpx/request.rb CHANGED
@@ -11,6 +11,8 @@ module HTTPX
11
11
  include Callbacks
12
12
  using URIExtensions
13
13
 
14
+ ALLOWED_URI_SCHEMES = %w[https http].freeze
15
+
14
16
  # default value used for "user-agent" header, when not overridden.
15
17
  USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
16
18
 
@@ -43,6 +45,8 @@ module HTTPX
43
45
 
44
46
  attr_writer :persistent
45
47
 
48
+ attr_reader :active_timeouts
49
+
46
50
  # will be +true+ when request body has been completely flushed.
47
51
  def_delegator :@body, :empty?
48
52
 
@@ -92,10 +96,13 @@ module HTTPX
92
96
  @uri = origin.merge("#{base_path}#{@uri}")
93
97
  end
94
98
 
99
+ raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
100
+
95
101
  @state = :idle
96
102
  @response = nil
97
103
  @peer_address = nil
98
104
  @persistent = @options.persistent
105
+ @active_timeouts = []
99
106
  end
100
107
 
101
108
  # the read timeout defined for this requet.
@@ -241,8 +248,10 @@ module HTTPX
241
248
  @body.rewind
242
249
  @response = nil
243
250
  @drainer = nil
251
+ @active_timeouts.clear
244
252
  when :headers
245
253
  return unless @state == :idle
254
+
246
255
  when :body
247
256
  return unless @state == :headers ||
248
257
  @state == :expect
@@ -263,6 +272,8 @@ module HTTPX
263
272
  return unless @state == :body
264
273
  when :done
265
274
  return if @state == :expect
275
+
276
+ @body.close
266
277
  end
267
278
  @state = nextstate
268
279
  emit(@state, self)
@@ -273,6 +284,15 @@ module HTTPX
273
284
  def expects?
274
285
  @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
275
286
  end
287
+
288
+ def set_timeout_callback(event, &callback)
289
+ clb = once(event, &callback)
290
+
291
+ # reset timeout callbacks when requests get rerouted to a different connection
292
+ once(:idle) do
293
+ callbacks(event).delete(clb)
294
+ end
295
+ end
276
296
  end
277
297
  end
278
298
 
@@ -82,7 +82,9 @@ module HTTPX
82
82
 
83
83
  if hostname.nil?
84
84
  hostname = connection.peer.host
85
- log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
85
+ log do
86
+ "resolver #{FAMILY_TYPES[@record_type]}: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
87
+ end if connection.peer.non_ascii_hostname
86
88
 
87
89
  hostname = @resolver.generate_candidates(hostname).each do |name|
88
90
  @queries[name.to_s] = connection
@@ -90,7 +92,7 @@ module HTTPX
90
92
  else
91
93
  @queries[hostname] = connection
92
94
  end
93
- log { "resolver: query #{FAMILY_TYPES[RECORD_TYPES[@family]]} for #{hostname}" }
95
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
94
96
 
95
97
  begin
96
98
  request = build_request(hostname)