httpx 1.3.4 → 1.4.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/lib/httpx/adapters/datadog.rb +55 -83
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +18 -6
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +12 -8
  10. data/lib/httpx/connection.rb +192 -22
  11. data/lib/httpx/errors.rb +12 -0
  12. data/lib/httpx/loggable.rb +5 -5
  13. data/lib/httpx/options.rb +26 -16
  14. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  15. data/lib/httpx/plugins/callbacks.rb +12 -2
  16. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  17. data/lib/httpx/plugins/content_digest.rb +202 -0
  18. data/lib/httpx/plugins/expect.rb +4 -3
  19. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  20. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  21. data/lib/httpx/plugins/h2c.rb +23 -20
  22. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  23. data/lib/httpx/plugins/persistent.rb +16 -0
  24. data/lib/httpx/plugins/proxy/http.rb +17 -19
  25. data/lib/httpx/plugins/proxy.rb +91 -93
  26. data/lib/httpx/plugins/retries.rb +5 -8
  27. data/lib/httpx/plugins/upgrade.rb +5 -10
  28. data/lib/httpx/plugins/webdav.rb +6 -0
  29. data/lib/httpx/plugins/xml.rb +76 -0
  30. data/lib/httpx/pool.rb +73 -244
  31. data/lib/httpx/request/body.rb +25 -26
  32. data/lib/httpx/request.rb +7 -1
  33. data/lib/httpx/resolver/https.rb +15 -20
  34. data/lib/httpx/resolver/multi.rb +34 -16
  35. data/lib/httpx/resolver/native.rb +66 -25
  36. data/lib/httpx/resolver/resolver.rb +59 -15
  37. data/lib/httpx/resolver/system.rb +31 -15
  38. data/lib/httpx/resolver.rb +21 -14
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +160 -95
  41. data/lib/httpx/session.rb +273 -140
  42. data/lib/httpx/transcoder/body.rb +15 -31
  43. data/lib/httpx/transcoder/gzip.rb +0 -3
  44. data/lib/httpx/transcoder/json.rb +14 -2
  45. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  46. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  47. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  48. data/lib/httpx/transcoder.rb +0 -1
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +20 -21
  51. data/sig/callbacks.rbs +0 -1
  52. data/sig/chainable.rbs +4 -0
  53. data/sig/connection/http2.rbs +1 -1
  54. data/sig/connection.rbs +29 -3
  55. data/sig/errors.rbs +6 -0
  56. data/sig/loggable.rbs +2 -0
  57. data/sig/options.rbs +7 -0
  58. data/sig/plugins/aws_sigv4.rbs +8 -2
  59. data/sig/plugins/content_digest.rbs +51 -0
  60. data/sig/plugins/cookies/cookie.rbs +9 -0
  61. data/sig/plugins/grpc/call.rbs +4 -0
  62. data/sig/plugins/persistent.rbs +4 -1
  63. data/sig/plugins/proxy/socks5.rbs +11 -3
  64. data/sig/plugins/proxy.rbs +18 -11
  65. data/sig/plugins/push_promise.rbs +3 -0
  66. data/sig/plugins/rate_limiter.rbs +2 -0
  67. data/sig/plugins/retries.rbs +1 -1
  68. data/sig/plugins/ssrf_filter.rbs +26 -0
  69. data/sig/plugins/webdav.rbs +23 -0
  70. data/sig/plugins/xml.rbs +37 -0
  71. data/sig/pool.rbs +25 -33
  72. data/sig/request/body.rbs +5 -9
  73. data/sig/resolver/multi.rbs +26 -1
  74. data/sig/resolver/native.rbs +2 -2
  75. data/sig/resolver/resolver.rbs +21 -2
  76. data/sig/resolver.rbs +5 -1
  77. data/sig/response/buffer.rbs +1 -1
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +47 -18
  80. data/sig/transcoder/body.rbs +2 -4
  81. data/sig/transcoder/chunker.rbs +1 -1
  82. data/sig/transcoder/deflate.rbs +1 -0
  83. data/sig/transcoder/form.rbs +8 -0
  84. data/sig/transcoder/gzip.rbs +4 -1
  85. data/sig/transcoder/utils/body_reader.rbs +3 -3
  86. data/sig/transcoder/utils/deflater.rbs +3 -3
  87. metadata +12 -4
  88. data/lib/httpx/transcoder/xml.rb +0 -52
  89. data/sig/transcoder/xml.rbs +0 -22
@@ -41,15 +41,18 @@ 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 :timers
46
+ attr_writer :current_selector
47
47
 
48
- attr_accessor :family
48
+ attr_accessor :current_session, :family
49
+
50
+ protected :sibling
49
51
 
50
52
  def initialize(uri, options)
51
- @origins = [uri.origin]
52
- @origin = Utils.to_uri(uri.origin)
53
+ @current_session = @current_selector = @sibling = @coalesced_connection = nil
54
+ @exhausted = @cloned = @main_sibling = false
55
+
53
56
  @options = Options.new(options)
54
57
  @type = initialize_type(uri, @options)
55
58
  @origins = [uri.origin]
@@ -58,6 +61,7 @@ module HTTPX
58
61
  @read_buffer = Buffer.new(@options.buffer_size)
59
62
  @write_buffer = Buffer.new(@options.buffer_size)
60
63
  @pending = []
64
+
61
65
  on(:error, &method(:on_error))
62
66
  if @options.io
63
67
  # if there's an already open IO, get its
@@ -68,6 +72,31 @@ module HTTPX
68
72
  else
69
73
  transition(:idle)
70
74
  end
75
+ on(:close) do
76
+ next if @exhausted # it'll reset
77
+
78
+ # may be called after ":close" above, so after the connection has been checked back in.
79
+ # next unless @current_session
80
+
81
+ next unless @current_session
82
+
83
+ @current_session.deselect_connection(self, @current_selector, @cloned)
84
+ end
85
+ on(:terminate) do
86
+ next if @exhausted # it'll reset
87
+
88
+ current_session = @current_session
89
+ current_selector = @current_selector
90
+
91
+ # may be called after ":close" above, so after the connection has been checked back in.
92
+ next unless current_session && current_selector
93
+
94
+ current_session.deselect_connection(self, current_selector)
95
+ end
96
+
97
+ on(:altsvc) do |alt_origin, origin, alt_params|
98
+ build_altsvc_connection(alt_origin, origin, alt_params)
99
+ end
71
100
 
72
101
  @inflight = 0
73
102
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
@@ -77,6 +106,10 @@ module HTTPX
77
106
  self.addresses = @options.addresses if @options.addresses
78
107
  end
79
108
 
109
+ def peer
110
+ @origin
111
+ end
112
+
80
113
  # this is a semi-private method, to be used by the resolver
81
114
  # to initiate the io object.
82
115
  def addresses=(addrs)
@@ -163,12 +196,23 @@ module HTTPX
163
196
  end
164
197
  end
165
198
 
199
+ def io_connected?
200
+ return @coalesced_connection.io_connected? if @coalesced_connection
201
+
202
+ @io && @io.state == :connected
203
+ end
204
+
166
205
  def connecting?
167
206
  @state == :idle
168
207
  end
169
208
 
170
209
  def inflight?
171
- @parser && !@parser.empty? && !@write_buffer.empty?
210
+ @parser && (
211
+ # parser may be dealing with other requests (possibly started from a different fiber)
212
+ !@parser.empty? ||
213
+ # connection may be doing connection termination handshake
214
+ !@write_buffer.empty?
215
+ )
172
216
  end
173
217
 
174
218
  def interests
@@ -184,6 +228,9 @@ module HTTPX
184
228
 
185
229
  return @parser.interests if @parser
186
230
 
231
+ nil
232
+ rescue StandardError => e
233
+ emit(:error, e)
187
234
  nil
188
235
  end
189
236
 
@@ -205,6 +252,9 @@ module HTTPX
205
252
  consume
206
253
  end
207
254
  nil
255
+ rescue StandardError => e
256
+ emit(:error, e)
257
+ raise e
208
258
  end
209
259
 
210
260
  def close
@@ -221,8 +271,9 @@ module HTTPX
221
271
 
222
272
  # bypasses the state machine to force closing of connections still connecting.
223
273
  # **only** used for Happy Eyeballs v2.
224
- def force_reset
274
+ def force_reset(cloned = false)
225
275
  @state = :closing
276
+ @cloned = cloned
226
277
  transition(:closed)
227
278
  end
228
279
 
@@ -235,6 +286,8 @@ module HTTPX
235
286
  end
236
287
 
237
288
  def send(request)
289
+ return @coalesced_connection.send(request) if @coalesced_connection
290
+
238
291
  if @parser && !@write_buffer.full?
239
292
  if @response_received_at && @keep_alive_timeout &&
240
293
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
@@ -255,6 +308,8 @@ module HTTPX
255
308
  end
256
309
 
257
310
  def timeout
311
+ return if @state == :closed || @state == :inactive
312
+
258
313
  return @timeout if @timeout
259
314
 
260
315
  return @options.timeout[:connect_timeout] if @state == :idle
@@ -295,12 +350,47 @@ module HTTPX
295
350
  on_error(error)
296
351
  end
297
352
 
353
+ def coalesced_connection=(connection)
354
+ @coalesced_connection = connection
355
+
356
+ close_sibling
357
+ connection.merge(self)
358
+ end
359
+
360
+ def sibling=(connection)
361
+ @sibling = connection
362
+
363
+ return unless connection
364
+
365
+ @main_sibling = connection.sibling.nil?
366
+
367
+ return unless @main_sibling
368
+
369
+ connection.sibling = self
370
+ end
371
+
372
+ def handle_connect_error(error)
373
+ @connect_error = error
374
+
375
+ return handle_error(error) unless @sibling && @sibling.connecting?
376
+
377
+ @sibling.merge(self)
378
+
379
+ force_reset(true)
380
+ end
381
+
298
382
  private
299
383
 
300
384
  def connect
301
385
  transition(:open)
302
386
  end
303
387
 
388
+ def disconnect
389
+ emit(:close)
390
+ @current_session = nil
391
+ @current_selector = nil
392
+ end
393
+
304
394
  def consume
305
395
  return unless @io
306
396
 
@@ -475,8 +565,27 @@ module HTTPX
475
565
  request.emit(:promise, parser, stream)
476
566
  end
477
567
  parser.on(:exhausted) do
478
- @pending.concat(parser.pending)
479
- emit(:exhausted)
568
+ @exhausted = true
569
+ current_session = @current_session
570
+ current_selector = @current_selector
571
+ begin
572
+ parser.close
573
+ @pending.concat(parser.pending)
574
+ ensure
575
+ @current_session = current_session
576
+ @current_selector = current_selector
577
+ end
578
+
579
+ case @state
580
+ when :closed
581
+ idling
582
+ @exhausted = false
583
+ when :closing
584
+ once(:closed) do
585
+ idling
586
+ @exhausted = false
587
+ end
588
+ end
480
589
  end
481
590
  parser.on(:origin) do |origin|
482
591
  @origins |= [origin]
@@ -492,8 +601,14 @@ module HTTPX
492
601
  end
493
602
  parser.on(:reset) do
494
603
  @pending.concat(parser.pending) unless parser.empty?
604
+ current_session = @current_session
605
+ current_selector = @current_selector
495
606
  reset
496
- idling unless @pending.empty?
607
+ unless @pending.empty?
608
+ idling
609
+ @current_session = current_session
610
+ @current_selector = current_selector
611
+ end
497
612
  end
498
613
  parser.on(:current_timeout) do
499
614
  @current_timeout = @timeout = parser.timeout
@@ -504,7 +619,15 @@ module HTTPX
504
619
  parser.on(:error) do |request, ex|
505
620
  case ex
506
621
  when MisdirectedRequestError
507
- emit(:misdirected, request)
622
+ current_session = @current_session
623
+ current_selector = @current_selector
624
+ parser.close
625
+
626
+ other_connection = current_session.find_connection(@origin, current_selector,
627
+ @options.merge(ssl: { alpn_protocols: %w[http/1.1] }))
628
+ other_connection.merge(self)
629
+ request.transition(:idle)
630
+ other_connection.send(request)
508
631
  else
509
632
  response = ErrorResponse.new(request, ex)
510
633
  request.response = response
@@ -529,15 +652,15 @@ module HTTPX
529
652
  # connect errors, exit gracefully
530
653
  error = ConnectionError.new(e.message)
531
654
  error.set_backtrace(e.backtrace)
532
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
655
+ handle_connect_error(error) if connecting?
533
656
  @state = :closed
534
- emit(:close)
657
+ disconnect
535
658
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
536
659
  # connect errors, exit gracefully
537
660
  handle_error(e)
538
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
661
+ handle_connect_error(e) if connecting?
539
662
  @state = :closed
540
- emit(:close)
663
+ disconnect
541
664
  end
542
665
 
543
666
  def handle_transition(nextstate)
@@ -550,7 +673,7 @@ module HTTPX
550
673
  return if @state == :closed
551
674
 
552
675
  @io.connect
553
- emit(:tcp_open, self) if @io.state == :connected
676
+ close_sibling if @io.state == :connected
554
677
 
555
678
  return unless @io.connected?
556
679
 
@@ -582,7 +705,8 @@ module HTTPX
582
705
  return unless @write_buffer.empty?
583
706
 
584
707
  purge_after_closed
585
- emit(:close) if @pending.empty?
708
+ disconnect if @pending.empty?
709
+
586
710
  when :already_open
587
711
  nextstate = :open
588
712
  # the first check for given io readiness must still use a timeout.
@@ -593,11 +717,29 @@ module HTTPX
593
717
  return unless @state == :inactive
594
718
 
595
719
  nextstate = :open
596
- emit(:activate)
720
+
721
+ # activate
722
+ @current_session.select_connection(self, @current_selector)
597
723
  end
598
724
  @state = nextstate
599
725
  end
600
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
+
601
743
  def purge_after_closed
602
744
  @io.close if @io
603
745
  @read_buffer.clear
@@ -617,12 +759,40 @@ module HTTPX
617
759
  end
618
760
  end
619
761
 
762
+ # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
763
+ def build_altsvc_connection(alt_origin, origin, alt_params)
764
+ # do not allow security downgrades on altsvc negotiation
765
+ return if @origin.scheme == "https" && alt_origin.scheme != "https"
766
+
767
+ altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
768
+
769
+ # altsvc already exists, somehow it wasn't advertised, probably noop
770
+ return unless altsvc
771
+
772
+ alt_options = @options.merge(ssl: @options.ssl.merge(hostname: URI(origin).host))
773
+
774
+ connection = @current_session.find_connection(alt_origin, @current_selector, alt_options)
775
+
776
+ # advertised altsvc is the same origin being used, ignore
777
+ return if connection == self
778
+
779
+ connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
780
+
781
+ log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
782
+
783
+ connection.merge(self)
784
+ terminate
785
+ rescue UnsupportedSchemeError
786
+ altsvc["noop"] = true
787
+ nil
788
+ end
789
+
620
790
  def build_socket(addrs = nil)
621
791
  case @type
622
792
  when "tcp"
623
- TCP.new(@origin, addrs, @options)
793
+ TCP.new(peer, addrs, @options)
624
794
  when "ssl"
625
- SSL.new(@origin, addrs, @options) do |sock|
795
+ SSL.new(peer, addrs, @options) do |sock|
626
796
  sock.ssl_session = @ssl_session
627
797
  sock.session_new_cb do |sess|
628
798
  @ssl_session = sess
@@ -635,7 +805,7 @@ module HTTPX
635
805
 
636
806
  path = String(path) if path
637
807
 
638
- UNIX.new(@origin, path, @options)
808
+ UNIX.new(peer, path, @options)
639
809
  else
640
810
  raise Error, "unsupported transport (#{@type})"
641
811
  end
@@ -734,7 +904,7 @@ module HTTPX
734
904
 
735
905
  def set_request_timeout(request, timeout, start_event, finish_events, &callback)
736
906
  request.once(start_event) do
737
- interval = @timers.after(timeout, callback)
907
+ interval = @current_selector.after(timeout, callback)
738
908
 
739
909
  Array(finish_events).each do |event|
740
910
  # clean up request timeouts if the connection errors out
data/lib/httpx/errors.rb CHANGED
@@ -29,6 +29,18 @@ module HTTPX
29
29
  end
30
30
  end
31
31
 
32
+ # Raise when it can't acquire a connection for a given origin.
33
+ class PoolTimeoutError < TimeoutError
34
+ attr_reader :origin
35
+
36
+ # initializes the +origin+ it refers to, and the
37
+ # +timeout+ causing the error.
38
+ def initialize(origin, timeout)
39
+ @origin = origin
40
+ super(timeout, "Timed out after #{timeout} seconds while waiting for a connection to #{origin}")
41
+ end
42
+ end
43
+
32
44
  # Error raised when there was a timeout establishing the connection to a server.
33
45
  # This may be raised due to timeouts during TCP and TLS (when applicable) connection
34
46
  # establishment.
@@ -13,11 +13,14 @@ module HTTPX
13
13
  white: 37,
14
14
  }.freeze
15
15
 
16
+ USE_DEBUG_LOG = ENV.key?("HTTPX_DEBUG")
17
+
16
18
  def log(level: @options.debug_level, color: nil, &msg)
17
- return unless @options.debug
18
19
  return unless @options.debug_level >= level
19
20
 
20
- debug_stream = @options.debug
21
+ debug_stream = @options.debug || ($stderr if USE_DEBUG_LOG)
22
+
23
+ return unless debug_stream
21
24
 
22
25
  message = (+"" << msg.call << "\n")
23
26
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
@@ -25,9 +28,6 @@ module HTTPX
25
28
  end
26
29
 
27
30
  def log_exception(ex, level: @options.debug_level, color: nil)
28
- return unless @options.debug
29
- return unless @options.debug_level >= level
30
-
31
31
  log(level: level, color: color) { ex.full_message }
32
32
  end
33
33
  end
data/lib/httpx/options.rb CHANGED
@@ -25,14 +25,14 @@ module HTTPX
25
25
  end
26
26
  rescue NotImplementedError
27
27
  [Socket::AF_INET]
28
- end
28
+ end.freeze
29
29
 
30
30
  DEFAULT_OPTIONS = {
31
31
  :max_requests => Float::INFINITY,
32
- :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
32
+ :debug => nil,
33
33
  :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
34
- :ssl => {},
35
- :http2_settings => { settings_enable_push: 0 },
34
+ :ssl => EMPTY_HASH,
35
+ :http2_settings => { settings_enable_push: 0 }.freeze,
36
36
  :fallback_protocol => "http/1.1",
37
37
  :supported_compression_formats => %w[gzip deflate],
38
38
  :decompress_response_body => true,
@@ -56,13 +56,15 @@ module HTTPX
56
56
  :response_class => Class.new(Response),
57
57
  :request_body_class => Class.new(Request::Body),
58
58
  :response_body_class => Class.new(Response::Body),
59
+ :pool_class => Class.new(Pool),
59
60
  :connection_class => Class.new(Connection),
60
61
  :options_class => Class.new(self),
61
62
  :transport => nil,
62
63
  :addresses => nil,
63
64
  :persistent => false,
64
65
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
65
- :resolver_options => { cache: true },
66
+ :resolver_options => { cache: true }.freeze,
67
+ :pool_options => EMPTY_HASH,
66
68
  :ip_families => ip_address_families,
67
69
  }.freeze
68
70
 
@@ -110,6 +112,7 @@ module HTTPX
110
112
  # :request_body_class :: class used to instantiate a request body
111
113
  # :response_body_class :: class used to instantiate a response body
112
114
  # :connection_class :: class used to instantiate connections
115
+ # :pool_class :: class used to instantiate the session connection pool
113
116
  # :options_class :: class used to instantiate options
114
117
  # :transport :: type of transport to use (set to "unix" for UNIX sockets)
115
118
  # :addresses :: bucket of peer addresses (can be a list of IP addresses, a hash of domain to list of adddresses;
@@ -118,7 +121,8 @@ module HTTPX
118
121
  # :persistent :: whether to persist connections in between requests (defaults to <tt>true</tt>)
119
122
  # :resolver_class :: which resolver to use (defaults to <tt>:native</tt>, can also be <tt>:system<tt> for
120
123
  # using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class)
121
- # :resolver_options :: hash of options passed to the resolver
124
+ # :resolver_options :: hash of options passed to the resolver. Accepted keys depend on the resolver type.
125
+ # :pool_options :: hash of options passed to the connection pool (See Pool#initialize).
122
126
  # :ip_families :: which socket families are supported (system-dependent)
123
127
  # :origin :: HTTP origin to set on requests with relative path (ex: "https://api.serv.com")
124
128
  # :base_path :: path to prefix given relative paths with (ex: "/v2")
@@ -215,6 +219,7 @@ module HTTPX
215
219
  ssl http2_settings
216
220
  request_class response_class headers_class request_body_class
217
221
  response_body_class connection_class options_class
222
+ pool_class pool_options
218
223
  io fallback_protocol debug debug_level resolver_class resolver_options
219
224
  compress_request_body decompress_response_body
220
225
  persistent
@@ -246,14 +251,6 @@ module HTTPX
246
251
  end
247
252
  end
248
253
 
249
- OTHER_LOOKUP = ->(obj, k, ivar_map) {
250
- case obj
251
- when Hash
252
- obj[ivar_map[k]]
253
- else
254
- obj.instance_variable_get(k)
255
- end
256
- }
257
254
  def merge(other)
258
255
  ivar_map = nil
259
256
  other_ivars = case other
@@ -266,12 +263,12 @@ module HTTPX
266
263
 
267
264
  return self if other_ivars.empty?
268
265
 
269
- return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == OTHER_LOOKUP[other, ivar, ivar_map] }
266
+ return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == access_option(other, ivar, ivar_map) }
270
267
 
271
268
  opts = dup
272
269
 
273
270
  other_ivars.each do |ivar|
274
- v = OTHER_LOOKUP[other, ivar, ivar_map]
271
+ v = access_option(other, ivar, ivar_map)
275
272
 
276
273
  unless v
277
274
  opts.instance_variable_set(ivar, v)
@@ -322,6 +319,10 @@ module HTTPX
322
319
  @response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
323
320
  @response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
324
321
  end
322
+ if defined?(pl::PoolMethods)
323
+ @pool_class = @pool_class.dup
324
+ @pool_class.__send__(:include, pl::PoolMethods)
325
+ end
325
326
  if defined?(pl::ConnectionMethods)
326
327
  @connection_class = @connection_class.dup
327
328
  @connection_class.__send__(:include, pl::ConnectionMethods)
@@ -346,5 +347,14 @@ module HTTPX
346
347
  instance_variable_set(:"@#{k}", value)
347
348
  end
348
349
  end
350
+
351
+ def access_option(obj, k, ivar_map)
352
+ case obj
353
+ when Hash
354
+ obj[ivar_map[k]]
355
+ else
356
+ obj.instance_variable_get(k)
357
+ end
358
+ end
349
359
  end
350
360
  end
@@ -89,7 +89,7 @@ module HTTPX
89
89
  sts = "#{algo_line}" \
90
90
  "\n#{datetime}" \
91
91
  "\n#{credential_scope}" \
92
- "\n#{hexdigest(creq)}"
92
+ "\n#{OpenSSL::Digest.new(@algorithm).hexdigest(creq)}"
93
93
 
94
94
  # signature
95
95
  k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
@@ -110,22 +110,38 @@ module HTTPX
110
110
  private
111
111
 
112
112
  def hexdigest(value)
113
- if value.respond_to?(:to_path)
114
- # files, pathnames
115
- OpenSSL::Digest.new(@algorithm).file(value.to_path).hexdigest
116
- elsif value.respond_to?(:each)
117
- digest = OpenSSL::Digest.new(@algorithm)
118
-
119
- mb_buffer = value.each.with_object("".b) do |chunk, buffer|
120
- buffer << chunk
121
- break if buffer.bytesize >= 1024 * 1024
122
- end
113
+ digest = OpenSSL::Digest.new(@algorithm)
123
114
 
124
- digest.update(mb_buffer)
125
- value.rewind
126
- digest.hexdigest
115
+ if value.respond_to?(:read)
116
+ if value.respond_to?(:to_path)
117
+ # files, pathnames
118
+ digest.file(value.to_path).hexdigest
119
+ else
120
+ # gzipped request bodies
121
+ raise Error, "request body must be rewindable" unless value.respond_to?(:rewind)
122
+
123
+ buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
124
+ begin
125
+ IO.copy_stream(value, buffer)
126
+ buffer.flush
127
+
128
+ digest.file(buffer.to_path).hexdigest
129
+ ensure
130
+ value.rewind
131
+ buffer.close
132
+ buffer.unlink
133
+ end
134
+ end
127
135
  else
128
- OpenSSL::Digest.new(@algorithm).hexdigest(value)
136
+ # error on endless generators
137
+ raise Error, "hexdigest for endless enumerators is not supported" if value.unbounded_body?
138
+
139
+ mb_buffer = value.each.with_object("".b) do |chunk, b|
140
+ b << chunk
141
+ break if b.bytesize >= 1024 * 1024
142
+ end
143
+
144
+ digest.hexdigest(mb_buffer)
129
145
  end
130
146
  end
131
147
 
@@ -142,7 +158,6 @@ module HTTPX
142
158
  def load_dependencies(*)
143
159
  require "set"
144
160
  require "digest/sha2"
145
- require "openssl"
146
161
  end
147
162
 
148
163
  def configure(klass)
@@ -31,12 +31,16 @@ module HTTPX
31
31
 
32
32
  private
33
33
 
34
- def init_connection(uri, options)
35
- connection = super
34
+ def do_init_connection(connection, selector)
35
+ super
36
36
  connection.on(:open) do
37
+ next unless connection.current_session == self
38
+
37
39
  emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
38
40
  end
39
41
  connection.on(:close) do
42
+ next unless connection.current_session == self
43
+
40
44
  emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
41
45
  end
42
46
 
@@ -84,6 +88,12 @@ module HTTPX
84
88
  rescue CallbackError => e
85
89
  raise e.cause
86
90
  end
91
+
92
+ def close(*)
93
+ super
94
+ rescue CallbackError => e
95
+ raise e.cause
96
+ end
87
97
  end
88
98
  end
89
99
  register_plugin :callbacks, Callbacks
@@ -32,11 +32,6 @@ module HTTPX
32
32
  @circuit_store = CircuitStore.new(@options)
33
33
  end
34
34
 
35
- def initialize_dup(orig)
36
- super
37
- @circuit_store = orig.instance_variable_get(:@circuit_store).dup
38
- end
39
-
40
35
  %i[circuit_open].each do |meth|
41
36
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
42
37
  def on_#{meth}(&blk) # def on_circuit_open(&blk)