httpx 1.4.0 → 1.4.4

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 (60) 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/doc/release_notes/1_4_3.md +11 -0
  6. data/doc/release_notes/1_4_4.md +14 -0
  7. data/lib/httpx/adapters/datadog.rb +55 -83
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +11 -1
  10. data/lib/httpx/callbacks.rb +2 -2
  11. data/lib/httpx/connection/http2.rb +33 -18
  12. data/lib/httpx/connection.rb +115 -55
  13. data/lib/httpx/errors.rb +3 -4
  14. data/lib/httpx/io/ssl.rb +6 -3
  15. data/lib/httpx/loggable.rb +13 -6
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  18. data/lib/httpx/plugins/expect.rb +1 -1
  19. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  20. data/lib/httpx/plugins/internal_telemetry.rb +21 -1
  21. data/lib/httpx/plugins/retries.rb +2 -2
  22. data/lib/httpx/plugins/stream.rb +42 -18
  23. data/lib/httpx/request/body.rb +9 -14
  24. data/lib/httpx/request.rb +37 -3
  25. data/lib/httpx/resolver/https.rb +4 -2
  26. data/lib/httpx/resolver/native.rb +111 -55
  27. data/lib/httpx/resolver/resolver.rb +18 -11
  28. data/lib/httpx/resolver/system.rb +3 -5
  29. data/lib/httpx/response.rb +9 -4
  30. data/lib/httpx/selector.rb +33 -23
  31. data/lib/httpx/session.rb +20 -49
  32. data/lib/httpx/timers.rb +16 -1
  33. data/lib/httpx/transcoder/body.rb +15 -31
  34. data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
  35. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  36. data/lib/httpx/version.rb +1 -1
  37. data/lib/httpx.rb +1 -1
  38. data/sig/callbacks.rbs +2 -2
  39. data/sig/connection/http2.rbs +4 -0
  40. data/sig/connection.rbs +19 -5
  41. data/sig/errors.rbs +3 -3
  42. data/sig/loggable.rbs +2 -2
  43. data/sig/plugins/stream.rbs +3 -0
  44. data/sig/pool.rbs +2 -0
  45. data/sig/request/body.rbs +0 -8
  46. data/sig/request.rbs +12 -0
  47. data/sig/resolver/native.rbs +6 -1
  48. data/sig/response.rbs +8 -3
  49. data/sig/selector.rbs +1 -0
  50. data/sig/session.rbs +2 -0
  51. data/sig/timers.rbs +15 -4
  52. data/sig/transcoder/body.rbs +1 -3
  53. data/sig/transcoder/json.rbs +1 -1
  54. data/sig/transcoder/multipart.rbs +1 -1
  55. data/sig/transcoder/utils/body_reader.rbs +1 -1
  56. data/sig/transcoder/utils/deflater.rbs +1 -2
  57. metadata +11 -9
  58. data/lib/httpx/session2.rb +0 -23
  59. data/lib/httpx/transcoder/utils/inflater.rb +0 -21
  60. data/sig/transcoder/utils/inflater.rbs +0 -12
@@ -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
@@ -290,6 +296,7 @@ module HTTPX
290
296
  @pending << request
291
297
  transition(:active) if @state == :inactive
292
298
  parser.ping
299
+ request.ping!
293
300
  return
294
301
  end
295
302
 
@@ -329,31 +336,54 @@ module HTTPX
329
336
  end
330
337
 
331
338
  def handle_socket_timeout(interval)
332
- @intervals.delete_if(&:elapsed?)
339
+ error = OperationTimeoutError.new(interval, "timed out while waiting on select")
340
+ error.set_backtrace(caller)
341
+ on_error(error)
342
+ end
333
343
 
334
- unless @intervals.empty?
335
- # remove the intervals which will elapse
344
+ def coalesced_connection=(connection)
345
+ @coalesced_connection = connection
336
346
 
337
- return
338
- end
347
+ close_sibling
348
+ connection.merge(self)
349
+ end
339
350
 
340
- error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
341
- error.set_backtrace(caller)
342
- on_error(error)
351
+ def sibling=(connection)
352
+ @sibling = connection
353
+
354
+ return unless connection
355
+
356
+ @main_sibling = connection.sibling.nil?
357
+
358
+ return unless @main_sibling
359
+
360
+ connection.sibling = self
343
361
  end
344
362
 
345
- private
363
+ def handle_connect_error(error)
364
+ @connect_error = error
346
365
 
347
- def connect
348
- transition(:open)
366
+ return handle_error(error) unless @sibling && @sibling.connecting?
367
+
368
+ @sibling.merge(self)
369
+
370
+ force_reset(true)
349
371
  end
350
372
 
351
373
  def disconnect
374
+ return unless @current_session && @current_selector
375
+
352
376
  emit(:close)
353
377
  @current_session = nil
354
378
  @current_selector = nil
355
379
  end
356
380
 
381
+ private
382
+
383
+ def connect
384
+ transition(:open)
385
+ end
386
+
357
387
  def consume
358
388
  return unless @io
359
389
 
@@ -394,6 +424,8 @@ module HTTPX
394
424
  siz = @io.read(@window_size, @read_buffer)
395
425
  log(level: 3, color: :cyan) { "IO READ: #{siz} bytes... (wsize: #{@window_size}, rbuffer: #{@read_buffer.bytesize})" }
396
426
  unless siz
427
+ @write_buffer.clear
428
+
397
429
  ex = EOFError.new("descriptor closed")
398
430
  ex.set_backtrace(caller)
399
431
  on_error(ex)
@@ -448,6 +480,8 @@ module HTTPX
448
480
  end
449
481
  log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
450
482
  unless siz
483
+ @write_buffer.clear
484
+
451
485
  ex = EOFError.new("descriptor closed")
452
486
  ex.set_backtrace(caller)
453
487
  on_error(ex)
@@ -531,20 +565,22 @@ module HTTPX
531
565
  @exhausted = true
532
566
  current_session = @current_session
533
567
  current_selector = @current_selector
534
- parser.close
535
- @pending.concat(parser.pending)
568
+ begin
569
+ parser.close
570
+ @pending.concat(parser.pending)
571
+ ensure
572
+ @current_session = current_session
573
+ @current_selector = current_selector
574
+ end
575
+
536
576
  case @state
537
577
  when :closed
538
578
  idling
539
579
  @exhausted = false
540
- @current_session = current_session
541
- @current_selector = current_selector
542
580
  when :closing
543
- once(:close) do
581
+ once(:closed) do
544
582
  idling
545
583
  @exhausted = false
546
- @current_session = current_session
547
- @current_selector = current_selector
548
584
  end
549
585
  end
550
586
  end
@@ -577,9 +613,9 @@ module HTTPX
577
613
  parser.on(:timeout) do |tout|
578
614
  @timeout = tout
579
615
  end
580
- parser.on(:error) do |request, ex|
581
- case ex
582
- when MisdirectedRequestError
616
+ parser.on(:error) do |request, error|
617
+ case error
618
+ when :http_1_1_required
583
619
  current_session = @current_session
584
620
  current_selector = @current_selector
585
621
  parser.close
@@ -589,11 +625,16 @@ module HTTPX
589
625
  other_connection.merge(self)
590
626
  request.transition(:idle)
591
627
  other_connection.send(request)
592
- else
593
- response = ErrorResponse.new(request, ex)
594
- request.response = response
595
- request.emit(:response, response)
628
+ next
629
+ when OperationTimeoutError
630
+ # request level timeouts should take precedence
631
+ next unless request.active_timeouts.empty?
596
632
  end
633
+
634
+ @inflight -= 1
635
+ response = ErrorResponse.new(request, error)
636
+ request.response = response
637
+ request.emit(:response, response)
597
638
  end
598
639
  end
599
640
 
@@ -613,14 +654,16 @@ module HTTPX
613
654
  # connect errors, exit gracefully
614
655
  error = ConnectionError.new(e.message)
615
656
  error.set_backtrace(e.backtrace)
616
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
657
+ handle_connect_error(error) if connecting?
617
658
  @state = :closed
659
+ purge_after_closed
618
660
  disconnect
619
661
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
620
662
  # connect errors, exit gracefully
621
663
  handle_error(e)
622
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
664
+ handle_connect_error(e) if connecting?
623
665
  @state = :closed
666
+ purge_after_closed
624
667
  disconnect
625
668
  end
626
669
 
@@ -629,12 +672,12 @@ module HTTPX
629
672
  when :idle
630
673
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
631
674
 
632
- @connected_at = nil
675
+ @connected_at = @response_received_at = nil
633
676
  when :open
634
677
  return if @state == :closed
635
678
 
636
679
  @io.connect
637
- emit(:tcp_open, self) if @io.state == :connected
680
+ close_sibling if @io.state == :connected
638
681
 
639
682
  return unless @io.connected?
640
683
 
@@ -667,6 +710,7 @@ module HTTPX
667
710
 
668
711
  purge_after_closed
669
712
  disconnect if @pending.empty?
713
+
670
714
  when :already_open
671
715
  nextstate = :open
672
716
  # the first check for given io readiness must still use a timeout.
@@ -677,11 +721,29 @@ module HTTPX
677
721
  return unless @state == :inactive
678
722
 
679
723
  nextstate = :open
680
- emit(:activate)
724
+
725
+ # activate
726
+ @current_session.select_connection(self, @current_selector)
681
727
  end
682
728
  @state = nextstate
683
729
  end
684
730
 
731
+ def close_sibling
732
+ return unless @sibling
733
+
734
+ if @sibling.io_connected?
735
+ reset
736
+ # TODO: transition connection to closed
737
+ end
738
+
739
+ unless @sibling.state == :closed
740
+ merge(@sibling) unless @main_sibling
741
+ @sibling.force_reset(true)
742
+ end
743
+
744
+ @sibling = nil
745
+ end
746
+
685
747
  def purge_after_closed
686
748
  @io.close if @io
687
749
  @read_buffer.clear
@@ -754,7 +816,7 @@ module HTTPX
754
816
  end
755
817
 
756
818
  def on_error(error, request = nil)
757
- if error.instance_of?(TimeoutError)
819
+ if error.is_a?(OperationTimeoutError)
758
820
 
759
821
  # inactive connections do not contribute to the select loop, therefore
760
822
  # they should not fail due to such errors.
@@ -783,6 +845,7 @@ module HTTPX
783
845
 
784
846
  return unless request
785
847
 
848
+ @inflight -= 1
786
849
  response = ErrorResponse.new(request, error)
787
850
  request.response = response
788
851
  request.emit(:response, response)
@@ -799,7 +862,7 @@ module HTTPX
799
862
 
800
863
  return if read_timeout.nil? || read_timeout.infinite?
801
864
 
802
- set_request_timeout(request, read_timeout, :done, :response) do
865
+ set_request_timeout(:read_timeout, request, read_timeout, :done, :response) do
803
866
  read_timeout_callback(request, read_timeout)
804
867
  end
805
868
  end
@@ -809,7 +872,7 @@ module HTTPX
809
872
 
810
873
  return if write_timeout.nil? || write_timeout.infinite?
811
874
 
812
- set_request_timeout(request, write_timeout, :headers, %i[done response]) do
875
+ set_request_timeout(:write_timeout, request, write_timeout, :headers, %i[done response]) do
813
876
  write_timeout_callback(request, write_timeout)
814
877
  end
815
878
  end
@@ -819,7 +882,7 @@ module HTTPX
819
882
 
820
883
  return if request_timeout.nil? || request_timeout.infinite?
821
884
 
822
- set_request_timeout(request, request_timeout, :headers, :complete) do
885
+ set_request_timeout(:request_timeout, request, request_timeout, :headers, :complete) do
823
886
  read_timeout_callback(request, request_timeout, RequestTimeoutError)
824
887
  end
825
888
  end
@@ -844,21 +907,18 @@ module HTTPX
844
907
  on_error(error, request)
845
908
  end
846
909
 
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)
910
+ def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
911
+ request.set_timeout_callback(start_event) do
912
+ timer = @current_selector.after(timeout, callback)
913
+ request.active_timeouts << label
850
914
 
851
915
  Array(finish_events).each do |event|
852
916
  # 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
917
+ request.set_timeout_callback(event) do
918
+ timer.cancel
919
+ request.active_timeouts.delete(label)
858
920
  end
859
921
  end
860
-
861
- @intervals << interval
862
922
  end
863
923
  end
864
924
 
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
 
@@ -112,8 +115,4 @@ module HTTPX
112
115
  @response.status
113
116
  end
114
117
  end
115
-
116
- # error raised when a request was sent a server which can't reproduce a response, and
117
- # has therefore returned an HTTP response using the 421 status code.
118
- class MisdirectedRequestError < HTTPError; end
119
118
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -92,9 +92,12 @@ module HTTPX
92
92
  end
93
93
 
94
94
  def connect
95
- super
96
- return if @state == :negotiated ||
97
- @state != :connected
95
+ return if @state == :negotiated
96
+
97
+ unless @state == :connected
98
+ super
99
+ return unless @state == :connected
100
+ end
98
101
 
99
102
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
100
103
  if (hostname_is_ip = (@ip == @sni_hostname))
@@ -15,20 +15,27 @@ module HTTPX
15
15
 
16
16
  USE_DEBUG_LOG = ENV.key?("HTTPX_DEBUG")
17
17
 
18
- def log(level: @options.debug_level, color: nil, &msg)
19
- return unless @options.debug_level >= level
18
+ def log(level: @options.debug_level, color: nil, debug_level: @options.debug_level, debug: @options.debug, &msg)
19
+ return unless debug_level >= level
20
20
 
21
- debug_stream = @options.debug || ($stderr if USE_DEBUG_LOG)
21
+ debug_stream = debug || ($stderr if USE_DEBUG_LOG)
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
29
36
 
30
- def log_exception(ex, level: @options.debug_level, color: nil)
31
- log(level: level, color: color) { ex.full_message }
37
+ def log_exception(ex, level: @options.debug_level, color: nil, debug_level: @options.debug_level, debug: @options.debug)
38
+ log(level: level, color: color, debug_level: debug_level, debug: debug) { ex.full_message }
32
39
  end
33
40
  end
34
41
  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
@@ -13,6 +13,12 @@ module HTTPX
13
13
  # by the end user in $http_init_time, different diff metrics can be shown. The "point of time" is calculated
14
14
  # using the monotonic clock.
15
15
  module InternalTelemetry
16
+ DEBUG_LEVEL = 3
17
+
18
+ def self.extra_options(options)
19
+ options.merge(debug_level: 3)
20
+ end
21
+
16
22
  module TrackTimeMethods
17
23
  private
18
24
 
@@ -28,7 +34,19 @@ module HTTPX
28
34
  after_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
29
35
  # $http_init_time = after_time
30
36
  elapsed = after_time - prev_time
31
- warn(+"\e[31m" << "[ELAPSED TIME]: #{label}: #{elapsed} (ms)" << "\e[0m")
37
+ # klass = self.class
38
+
39
+ # until (class_name = klass.name)
40
+ # klass = klass.superclass
41
+ # end
42
+ log(
43
+ level: DEBUG_LEVEL,
44
+ color: :red,
45
+ debug_level: @options ? @options.debug_level : DEBUG_LEVEL,
46
+ debug: nil
47
+ ) do
48
+ "[ELAPSED TIME]: #{label}: #{elapsed} (ms)" << "\e[0m"
49
+ end
32
50
  end
33
51
  end
34
52
 
@@ -88,6 +106,7 @@ module HTTPX
88
106
 
89
107
  module RequestMethods
90
108
  def self.included(klass)
109
+ klass.prepend Loggable
91
110
  klass.prepend TrackTimeMethods
92
111
  super
93
112
  end
@@ -114,6 +133,7 @@ module HTTPX
114
133
 
115
134
  module PoolMethods
116
135
  def self.included(klass)
136
+ klass.prepend Loggable
117
137
  klass.prepend TrackTimeMethods
118
138
  super
119
139
  end
@@ -110,7 +110,7 @@ module HTTPX
110
110
  )
111
111
  __try_partial_retry(request, response)
112
112
  log { "failed to get response, #{request.retries} tries to go..." }
113
- request.retries -= 1
113
+ request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
114
114
  request.transition(:idle)
115
115
 
116
116
  retry_after = options.retry_after
@@ -167,7 +167,7 @@ module HTTPX
167
167
  unless response.headers.key?("accept-ranges") &&
168
168
  response.headers["accept-ranges"] == "bytes" && # there's nothing else supported though...
169
169
  (original_body = response.body)
170
- response.close if response.respond_to?(:close)
170
+ response.body.close
171
171
  return
172
172
  end
173
173
 
@@ -4,27 +4,39 @@ module HTTPX
4
4
  class StreamResponse
5
5
  def initialize(request, session)
6
6
  @request = request
7
+ @options = @request.options
7
8
  @session = session
8
- @response = nil
9
+ @response_enum = nil
10
+ @buffered_chunks = []
9
11
  end
10
12
 
11
13
  def each(&block)
12
14
  return enum_for(__method__) unless block
13
15
 
16
+ if (response_enum = @response_enum)
17
+ @response_enum = nil
18
+ # streaming already started, let's finish it
19
+
20
+ while (chunk = @buffered_chunks.shift)
21
+ block.call(chunk)
22
+ end
23
+
24
+ # consume enum til the end
25
+ begin
26
+ while (chunk = response_enum.next)
27
+ block.call(chunk)
28
+ end
29
+ rescue StopIteration
30
+ return
31
+ end
32
+ end
33
+
14
34
  @request.stream = self
15
35
 
16
36
  begin
17
37
  @on_chunk = block
18
38
 
19
- if @request.response
20
- # if we've already started collecting the payload, yield it first
21
- # before proceeding.
22
- body = @request.response.body
23
-
24
- body.each do |chunk|
25
- on_chunk(chunk)
26
- end
27
- end
39
+ response = @session.request(@request)
28
40
 
29
41
  response.raise_for_status
30
42
  ensure
@@ -64,27 +76,39 @@ module HTTPX
64
76
  # :nocov:
65
77
 
66
78
  def to_s
67
- response.to_s
79
+ if @request.response
80
+ @request.response.to_s
81
+ else
82
+ @buffered_chunks.join
83
+ end
68
84
  end
69
85
 
70
86
  private
71
87
 
72
88
  def response
73
- return @response if @response
74
-
75
89
  @request.response || begin
76
- @response = @session.request(@request)
90
+ response_enum = each
91
+ while (chunk = response_enum.next)
92
+ @buffered_chunks << chunk
93
+ break if @request.response
94
+ end
95
+ @response_enum = response_enum
96
+ @request.response
77
97
  end
78
98
  end
79
99
 
80
- def respond_to_missing?(meth, *args)
81
- response.respond_to?(meth, *args) || super
100
+ def respond_to_missing?(meth, include_private)
101
+ if (response = @request.response)
102
+ response.respond_to_missing?(meth, include_private)
103
+ else
104
+ @options.response_class.method_defined?(meth) || (include_private && @options.response_class.private_method_defined?(meth))
105
+ end || super
82
106
  end
83
107
 
84
- def method_missing(meth, *args, &block)
108
+ def method_missing(meth, *args, **kwargs, &block)
85
109
  return super unless response.respond_to?(meth)
86
110
 
87
- response.__send__(meth, *args, &block)
111
+ response.__send__(meth, *args, **kwargs, &block)
88
112
  end
89
113
  end
90
114
 
@@ -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