httpx 1.6.2 → 1.7.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/sentry.rb +1 -1
  8. data/lib/httpx/altsvc.rb +3 -1
  9. data/lib/httpx/connection/http1.rb +14 -15
  10. data/lib/httpx/connection/http2.rb +16 -15
  11. data/lib/httpx/connection.rb +118 -110
  12. data/lib/httpx/domain_name.rb +1 -1
  13. data/lib/httpx/extensions.rb +0 -14
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/ssl.rb +1 -1
  16. data/lib/httpx/loggable.rb +14 -2
  17. data/lib/httpx/options.rb +60 -17
  18. data/lib/httpx/plugins/auth/digest.rb +44 -4
  19. data/lib/httpx/plugins/auth.rb +87 -4
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  21. data/lib/httpx/plugins/callbacks.rb +15 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +162 -56
  30. data/lib/httpx/plugins/proxy/http.rb +37 -9
  31. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  32. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  33. data/lib/httpx/plugins/response_cache.rb +16 -9
  34. data/lib/httpx/plugins/retries.rb +55 -16
  35. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  36. data/lib/httpx/plugins/stream.rb +59 -8
  37. data/lib/httpx/plugins/stream_bidi.rb +87 -22
  38. data/lib/httpx/pool.rb +65 -21
  39. data/lib/httpx/request.rb +13 -14
  40. data/lib/httpx/resolver/https.rb +100 -34
  41. data/lib/httpx/resolver/multi.rb +12 -27
  42. data/lib/httpx/resolver/native.rb +68 -38
  43. data/lib/httpx/resolver/resolver.rb +46 -29
  44. data/lib/httpx/resolver/system.rb +63 -39
  45. data/lib/httpx/resolver.rb +97 -29
  46. data/lib/httpx/response/body.rb +2 -0
  47. data/lib/httpx/response.rb +22 -6
  48. data/lib/httpx/selector.rb +44 -20
  49. data/lib/httpx/session.rb +23 -33
  50. data/lib/httpx/transcoder/body.rb +1 -1
  51. data/lib/httpx/transcoder/deflate.rb +13 -8
  52. data/lib/httpx/transcoder/json.rb +1 -1
  53. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  54. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  55. data/lib/httpx/transcoder/multipart.rb +16 -8
  56. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  57. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  58. data/lib/httpx/transcoder.rb +4 -6
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/altsvc.rbs +3 -0
  61. data/sig/chainable.rbs +3 -3
  62. data/sig/connection.rbs +13 -6
  63. data/sig/loggable.rbs +5 -1
  64. data/sig/options.rbs +6 -2
  65. data/sig/plugins/auth/digest.rbs +6 -0
  66. data/sig/plugins/auth.rbs +28 -4
  67. data/sig/plugins/basic_auth.rbs +3 -3
  68. data/sig/plugins/callbacks.rbs +3 -0
  69. data/sig/plugins/digest_auth.rbs +2 -4
  70. data/sig/plugins/fiber_concurrency.rbs +6 -0
  71. data/sig/plugins/ntlm_auth.rbs +2 -2
  72. data/sig/plugins/oauth.rbs +46 -15
  73. data/sig/plugins/rate_limiter.rbs +1 -1
  74. data/sig/plugins/response_cache/file_store.rbs +2 -0
  75. data/sig/plugins/response_cache.rbs +4 -0
  76. data/sig/plugins/retries.rbs +8 -2
  77. data/sig/plugins/stream.rbs +13 -3
  78. data/sig/plugins/stream_bidi.rbs +5 -7
  79. data/sig/pool.rbs +1 -1
  80. data/sig/resolver/https.rbs +7 -0
  81. data/sig/resolver/multi.rbs +2 -9
  82. data/sig/resolver/native.rbs +1 -1
  83. data/sig/resolver/resolver.rbs +9 -8
  84. data/sig/resolver/system.rbs +4 -2
  85. data/sig/resolver.rbs +12 -3
  86. data/sig/response.rbs +3 -0
  87. data/sig/selector.rbs +2 -0
  88. data/sig/session.rbs +8 -8
  89. data/sig/transcoder/multipart.rbs +4 -2
  90. data/sig/transcoder.rbs +5 -1
  91. metadata +5 -1
@@ -34,8 +34,6 @@ module HTTPX
34
34
 
35
35
  using URIExtensions
36
36
 
37
- def_delegator :@io, :closed?
38
-
39
37
  def_delegator :@write_buffer, :empty?
40
38
 
41
39
  attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling
@@ -48,9 +46,9 @@ module HTTPX
48
46
 
49
47
  def initialize(uri, options)
50
48
  @current_session = @current_selector =
51
- @parser = @sibling = @coalesced_connection =
52
- @family = @io = @ssl_session = @timeout =
53
- @connected_at = @response_received_at = nil
49
+ @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
+ @family = @io = @ssl_session = @timeout =
51
+ @connected_at = @response_received_at = nil
54
52
 
55
53
  @exhausted = @cloned = @main_sibling = false
56
54
 
@@ -65,7 +63,6 @@ module HTTPX
65
63
  @inflight = 0
66
64
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
67
65
 
68
- on(:error, &method(:on_error))
69
66
  if @options.io
70
67
  # if there's an already open IO, get its
71
68
  # peer address, and force-initiate the parser
@@ -75,32 +72,6 @@ module HTTPX
75
72
  else
76
73
  transition(:idle)
77
74
  end
78
- on(:close) do
79
- next if @exhausted # it'll reset
80
-
81
- # may be called after ":close" above, so after the connection has been checked back in.
82
- # next unless @current_session
83
-
84
- next unless @current_session
85
-
86
- @current_session.deselect_connection(self, @current_selector, @cloned)
87
- end
88
- on(:terminate) do
89
- next if @exhausted # it'll reset
90
-
91
- current_session = @current_session
92
- current_selector = @current_selector
93
-
94
- # may be called after ":close" above, so after the connection has been checked back in.
95
- next unless current_session && current_selector
96
-
97
- current_session.deselect_connection(self, current_selector)
98
- end
99
-
100
- on(:altsvc) do |alt_origin, origin, alt_params|
101
- build_altsvc_connection(alt_origin, origin, alt_params)
102
- end
103
-
104
75
  self.addresses = @options.addresses if @options.addresses
105
76
  end
106
77
 
@@ -129,14 +100,13 @@ module HTTPX
129
100
  def match?(uri, options)
130
101
  return false if !used? && (@state == :closing || @state == :closed)
131
102
 
132
- (
133
- @origins.include?(uri.origin) &&
103
+ @origins.include?(uri.origin) &&
134
104
  # if there is more than one origin to match, it means that this connection
135
105
  # was the result of coalescing. To prevent blind trust in the case where the
136
106
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
137
107
  # SSL certificate
138
- (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
139
- ) && @options == options
108
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
109
+ @options == options
140
110
  end
141
111
 
142
112
  def mergeable?(connection)
@@ -158,6 +128,10 @@ module HTTPX
158
128
  connection.merge(self)
159
129
  end
160
130
 
131
+ def coalesced?
132
+ @coalesced_connection
133
+ end
134
+
161
135
  # coalescable connections need to be mergeable!
162
136
  # but internally, #mergeable? is called before #coalescable?
163
137
  def coalescable?(connection)
@@ -171,10 +145,6 @@ module HTTPX
171
145
  end
172
146
  end
173
147
 
174
- def create_idle(options = {})
175
- self.class.new(@origin, @options.merge(options))
176
- end
177
-
178
148
  def merge(connection)
179
149
  @origins |= connection.instance_variable_get(:@origins)
180
150
  if @ssl_session.nil? && connection.ssl_session
@@ -231,7 +201,7 @@ module HTTPX
231
201
 
232
202
  nil
233
203
  rescue StandardError => e
234
- emit(:error, e)
204
+ on_error(e)
235
205
  nil
236
206
  end
237
207
 
@@ -259,7 +229,9 @@ module HTTPX
259
229
  nil
260
230
  rescue StandardError => e
261
231
  @write_buffer.clear
262
- emit(:error, e)
232
+ on_error(e)
233
+ rescue Exception => e # rubocop:disable Lint/RescueException
234
+ force_close(true)
263
235
  raise e
264
236
  end
265
237
 
@@ -273,7 +245,7 @@ module HTTPX
273
245
  case @state
274
246
  when :idle
275
247
  purge_after_closed
276
- emit(:terminate)
248
+ disconnect
277
249
  when :closed
278
250
  @connected_at = nil
279
251
  end
@@ -281,6 +253,23 @@ module HTTPX
281
253
  close
282
254
  end
283
255
 
256
+ # bypasses state machine rules while setting the connection in the
257
+ # :closed state.
258
+ def force_close(delete_pending = false)
259
+ if delete_pending
260
+ @pending.clear
261
+ elsif (parser = @parser)
262
+ enqueue_pending_requests_from_parser(parser)
263
+ end
264
+ return if @state == :closed
265
+
266
+ @state = :closed
267
+ @write_buffer.clear
268
+ purge_after_closed
269
+ disconnect
270
+ emit(:force_closed, delete_pending)
271
+ end
272
+
284
273
  # bypasses the state machine to force closing of connections still connecting.
285
274
  # **only** used for Happy Eyeballs v2.
286
275
  def force_reset(cloned = false)
@@ -368,18 +357,40 @@ module HTTPX
368
357
  end
369
358
 
370
359
  def handle_connect_error(error)
371
- return handle_error(error) unless @sibling && @sibling.connecting?
360
+ return on_error(error) unless @sibling && @sibling.connecting?
372
361
 
373
362
  @sibling.merge(self)
374
363
 
375
364
  force_reset(true)
376
365
  end
377
366
 
367
+ # disconnects from the current session it's attached to
378
368
  def disconnect
379
- return unless @current_session && @current_selector
369
+ return if @exhausted # it'll reset
370
+
371
+ return unless (current_session = @current_session) && (current_selector = @current_selector)
380
372
 
381
- emit(:close)
382
373
  @current_session = @current_selector = nil
374
+
375
+ current_session.deselect_connection(self, current_selector, @cloned)
376
+ end
377
+
378
+ def on_error(error, request = nil)
379
+ if error.is_a?(OperationTimeoutError)
380
+
381
+ # inactive connections do not contribute to the select loop, therefore
382
+ # they should not fail due to such errors.
383
+ return if @state == :inactive
384
+
385
+ if @timeout
386
+ @timeout -= error.timeout
387
+ return unless @timeout <= 0
388
+ end
389
+
390
+ error = error.to_connection_error if connecting?
391
+ end
392
+ handle_error(error, request)
393
+ reset
383
394
  end
384
395
 
385
396
  # :nocov:
@@ -417,7 +428,11 @@ module HTTPX
417
428
  # * the number of pending requests
418
429
  # * whether the write buffer has bytes (i.e. for close handshake)
419
430
  if @pending.empty? && @inflight.zero? && @write_buffer.empty?
420
- log(level: 3) { "NO MORE REQUESTS..." }
431
+ log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
432
+
433
+ # terminate if an altsvc connection has been established
434
+ terminate if @altsvc_connection
435
+
421
436
  return
422
437
  end
423
438
 
@@ -462,7 +477,14 @@ module HTTPX
462
477
  break if @state == :closing || @state == :closed
463
478
 
464
479
  # exit #consume altogether if all outstanding requests have been dealt with
465
- return if @pending.empty? && @inflight.zero?
480
+ if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
481
+ log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
482
+
483
+ # terminate if an altsvc connection has been established
484
+ terminate if @altsvc_connection
485
+
486
+ return
487
+ end
466
488
  end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
467
489
 
468
490
  #
@@ -555,6 +577,17 @@ module HTTPX
555
577
  request.ping!
556
578
  end
557
579
 
580
+ def enqueue_pending_requests_from_parser(parser)
581
+ parser_pending_requests = parser.pending
582
+
583
+ return if parser_pending_requests.empty?
584
+
585
+ # the connection will be reused, so parser requests must come
586
+ # back to the pending list before the parser is reset.
587
+ @inflight -= parser_pending_requests.size
588
+ @pending.unshift(*parser_pending_requests)
589
+ end
590
+
558
591
  def build_parser(protocol = @io.protocol)
559
592
  parser = parser_type(protocol).new(@write_buffer, @options)
560
593
  set_parser_callbacks(parser)
@@ -564,7 +597,7 @@ module HTTPX
564
597
  def set_parser_callbacks(parser)
565
598
  parser.on(:response) do |request, response|
566
599
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
567
- emit(:altsvc, alt_origin, origin, alt_params)
600
+ build_altsvc_connection(alt_origin, origin, alt_params)
568
601
  end
569
602
  @response_received_at = Utils.now
570
603
  @inflight -= 1
@@ -572,7 +605,7 @@ module HTTPX
572
605
  request.emit(:response, response)
573
606
  end
574
607
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
575
- emit(:altsvc, alt_origin, origin, alt_params)
608
+ build_altsvc_connection(alt_origin, origin, alt_params)
576
609
  end
577
610
 
578
611
  parser.on(:pong, &method(:send_pending))
@@ -581,50 +614,31 @@ module HTTPX
581
614
  request.emit(:promise, parser, stream)
582
615
  end
583
616
  parser.on(:exhausted) do
617
+ enqueue_pending_requests_from_parser(parser)
618
+
584
619
  @exhausted = true
585
- current_session = @current_session
586
- current_selector = @current_selector
587
- begin
588
- parser.close
589
- @pending.concat(parser.pending)
590
- ensure
591
- @current_session = current_session
592
- @current_selector = current_selector
593
- end
620
+ parser.close
594
621
 
595
- case @state
596
- when :closed
597
- idling
598
- @exhausted = false
599
- when :closing
600
- once(:closed) do
601
- idling
602
- @exhausted = false
603
- end
604
- end
622
+ idling
623
+ @exhausted = false
605
624
  end
606
625
  parser.on(:origin) do |origin|
607
626
  @origins |= [origin]
608
627
  end
609
- parser.on(:close) do |force|
610
- if force
611
- reset
612
- emit(:terminate)
613
- end
628
+ parser.on(:close) do
629
+ reset
630
+ disconnect
614
631
  end
615
632
  parser.on(:close_handshake) do
616
- consume
633
+ consume unless @state == :closed
617
634
  end
618
635
  parser.on(:reset) do
619
- @pending.concat(parser.pending) unless parser.empty?
620
- current_session = @current_session
621
- current_selector = @current_selector
636
+ enqueue_pending_requests_from_parser(parser)
637
+
622
638
  reset
623
- unless @pending.empty?
624
- idling
625
- @current_session = current_session
626
- @current_selector = current_selector
627
- end
639
+ # :reset event only fired in http/1.1, so this guarantees
640
+ # that the connection will be closed here.
641
+ idling unless @pending.empty?
628
642
  end
629
643
  parser.on(:current_timeout) do
630
644
  @current_timeout = @timeout = parser.timeout
@@ -674,16 +688,12 @@ module HTTPX
674
688
  error = ConnectionError.new(e.message)
675
689
  error.set_backtrace(e.backtrace)
676
690
  handle_connect_error(error) if connecting?
677
- @state = :closed
678
- purge_after_closed
679
- disconnect
691
+ force_close
680
692
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
681
693
  # connect errors, exit gracefully
682
694
  handle_error(e)
683
695
  handle_connect_error(e) if connecting?
684
- @state = :closed
685
- purge_after_closed
686
- disconnect
696
+ force_close
687
697
  end
688
698
 
689
699
  def handle_transition(nextstate)
@@ -711,6 +721,8 @@ module HTTPX
711
721
 
712
722
  # do not deactivate connection in use
713
723
  return if @inflight.positive? || @parser.waiting_for_ping?
724
+
725
+ disconnect
714
726
  when :closing
715
727
  return unless @state == :idle || @state == :open
716
728
 
@@ -785,6 +797,8 @@ module HTTPX
785
797
 
786
798
  # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
787
799
  def build_altsvc_connection(alt_origin, origin, alt_params)
800
+ return if @altsvc_connection
801
+
788
802
  # do not allow security downgrades on altsvc negotiation
789
803
  return if @origin.scheme == "https" && alt_origin.scheme != "https"
790
804
 
@@ -802,10 +816,11 @@ module HTTPX
802
816
 
803
817
  connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
804
818
 
805
- log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
819
+ @altsvc_connection = connection
820
+
821
+ log(level: 1) { "#{origin}: alt-svc connection##{connection.object_id} established to #{alt_origin}" }
806
822
 
807
823
  connection.merge(self)
808
- terminate
809
824
  rescue UnsupportedSchemeError
810
825
  altsvc["noop"] = true
811
826
  nil
@@ -835,26 +850,8 @@ module HTTPX
835
850
  end
836
851
  end
837
852
 
838
- def on_error(error, request = nil)
839
- if error.is_a?(OperationTimeoutError)
840
-
841
- # inactive connections do not contribute to the select loop, therefore
842
- # they should not fail due to such errors.
843
- return if @state == :inactive
844
-
845
- if @timeout
846
- @timeout -= error.timeout
847
- return unless @timeout <= 0
848
- end
849
-
850
- error = error.to_connection_error if connecting?
851
- end
852
- handle_error(error, request)
853
- reset
854
- end
855
-
856
853
  def handle_error(error, request = nil)
857
- parser.handle_error(error, request) if @parser && parser.respond_to?(:handle_error)
854
+ parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
858
855
  while (req = @pending.shift)
859
856
  next if request && req == request
860
857
 
@@ -929,6 +926,17 @@ module HTTPX
929
926
 
930
927
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
931
928
  request.set_timeout_callback(start_event) do
929
+ unless @current_selector
930
+ raise Error, "request has been resend to an out-of-session connection, and this " \
931
+ "should never happen!!! Please report this error! " \
932
+ "(state:#{@state}, " \
933
+ "parser?:#{!!@parser}, " \
934
+ "bytes in write buffer?:#{!@write_buffer.empty?}, " \
935
+ "cloned?:#{@cloned}, " \
936
+ "sibling?:#{!!@sibling}, " \
937
+ "coalesced?:#{coalesced?})"
938
+ end
939
+
932
940
  timer = @current_selector.after(timeout, callback)
933
941
  request.active_timeouts << label
934
942
 
@@ -55,7 +55,7 @@ module HTTPX
55
55
  def new(domain)
56
56
  return domain if domain.is_a?(self)
57
57
 
58
- super(domain)
58
+ super
59
59
  end
60
60
 
61
61
  # Normalizes a _domain_ using the Punycode algorithm as necessary.
@@ -4,20 +4,6 @@ require "uri"
4
4
 
5
5
  module HTTPX
6
6
  module ArrayExtensions
7
- module FilterMap
8
- refine Array do
9
- # Ruby 2.7 backport
10
- def filter_map
11
- return to_enum(:filter_map) unless block_given?
12
-
13
- each_with_object([]) do |item, res|
14
- processed = yield(item)
15
- res << processed if processed
16
- end
17
- end
18
- end unless Array.method_defined?(:filter_map)
19
- end
20
-
21
7
  module Intersect
22
8
  refine Array do
23
9
  # Ruby 3.1 backport
data/lib/httpx/headers.rb CHANGED
@@ -42,12 +42,12 @@ module HTTPX
42
42
  # dupped initialization
43
43
  def initialize_dup(orig)
44
44
  super
45
- @headers = orig.instance_variable_get(:@headers).dup
45
+ @headers = orig.instance_variable_get(:@headers).transform_values(&:dup)
46
46
  end
47
47
 
48
48
  # freezes the headers hash
49
49
  def freeze
50
- @headers.freeze
50
+ @headers.each_value(&:freeze).freeze
51
51
  super
52
52
  end
53
53
 
data/lib/httpx/io/ssl.rb CHANGED
@@ -98,7 +98,7 @@ module HTTPX
98
98
  end
99
99
 
100
100
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
101
- if (hostname_is_ip = (@ip == @sni_hostname))
101
+ if (hostname_is_ip = (@ip == @sni_hostname)) && @ctx.verify_hostname
102
102
  # IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
103
103
  @sni_hostname = @ip.to_string
104
104
  # IP addresses in SNI is not valid per RFC 6066, section 3.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fiber" if RUBY_VERSION < "3.0.0"
4
+
3
5
  module HTTPX
4
6
  module Loggable
5
7
  COLORS = {
@@ -34,7 +36,7 @@ module HTTPX
34
36
  klass = klass.superclass
35
37
  end
36
38
 
37
- message = +"(pid:#{Process.pid}, " \
39
+ message = +"(time:#{Time.now.utc}, pid:#{Process.pid}, " \
38
40
  "tid:#{Thread.current.object_id}, " \
39
41
  "fid:#{Fiber.current.object_id}, " \
40
42
  "self:#{class_name}##{object_id}) "
@@ -47,7 +49,17 @@ module HTTPX
47
49
  log(level: level, color: color, debug_level: debug_level, debug: debug) { ex.full_message }
48
50
  end
49
51
 
50
- def log_redact(text, should_redact = @options.debug_redact)
52
+ def log_redact_headers(text)
53
+ log_redact(text, @options.debug_redact == :headers)
54
+ end
55
+
56
+ def log_redact_body(text)
57
+ log_redact(text, @options.debug_redact == :body)
58
+ end
59
+
60
+ def log_redact(text, should_redact)
61
+ should_redact ||= @options.debug_redact == true
62
+
51
63
  return text.to_s unless should_redact
52
64
 
53
65
  "[REDACTED]"
data/lib/httpx/options.rb CHANGED
@@ -13,6 +13,9 @@ module HTTPX
13
13
  CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
14
14
  REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
15
15
 
16
+ # default value used for "user-agent" header, when not overridden.
17
+ USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
18
+
16
19
  @options_names = []
17
20
 
18
21
  class << self
@@ -144,6 +147,7 @@ module HTTPX
144
147
  instance_variable_set(:"@#{k}", value)
145
148
  end
146
149
 
150
+ do_initialize
147
151
  freeze
148
152
  end
149
153
 
@@ -184,33 +188,40 @@ module HTTPX
184
188
  end
185
189
 
186
190
  def merge(other)
187
- ivar_map = nil
188
- other_ivars = case other
189
- when Options
190
- other.instance_variables
191
- else
192
- other = Hash[other] unless other.is_a?(Hash)
193
- ivar_map = other.keys.to_h { |k| [:"@#{k}", k] }
194
- ivar_map.keys
195
- end
191
+ if (is_options = other.is_a?(Options))
192
+
193
+ return self if eql?(other)
196
194
 
197
- return self if other_ivars.empty?
195
+ opts_names = other.class.options_names
196
+
197
+ return self if opts_names.all? { |opt| public_send(opt) == other.public_send(opt) }
198
+
199
+ other_opts = opts_names
200
+ else
201
+ other_opts = other # : Hash[Symbol, untyped]
202
+ other_opts = Hash[other] unless other.is_a?(Hash)
198
203
 
199
- return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == access_option(other, ivar, ivar_map) }
204
+ return self if other_opts.empty?
205
+
206
+ return self if other_opts.all? { |opt, v| !respond_to?(opt) || public_send(opt) == v }
207
+ end
200
208
 
201
209
  opts = dup
202
210
 
203
- other_ivars.each do |ivar|
204
- v = access_option(other, ivar, ivar_map)
211
+ other_opts.each do |opt, v|
212
+ next unless respond_to?(opt)
213
+
214
+ v = other.public_send(opt) if is_options
215
+ ivar = :"@#{opt}"
205
216
 
206
217
  unless v
207
218
  opts.instance_variable_set(ivar, v)
208
219
  next
209
220
  end
210
221
 
211
- v = opts.__send__(:"option_#{ivar[1..-1]}", v)
222
+ v = opts.__send__(:"option_#{opt}", v)
212
223
 
213
- orig_v = instance_variable_get(ivar)
224
+ orig_v = public_send(opt)
214
225
 
215
226
  v = orig_v.merge(v) if orig_v.respond_to?(:merge) && v.respond_to?(:merge)
216
227
 
@@ -369,11 +380,26 @@ module HTTPX
369
380
  end
370
381
 
371
382
  def option_headers(value)
383
+ value = value.dup if value.frozen?
384
+
372
385
  headers_class.new(value)
373
386
  end
374
387
 
375
388
  def option_timeout(value)
376
- Hash[value]
389
+ timeout_hash = Hash[value]
390
+
391
+ default_timeouts = DEFAULT_OPTIONS[:timeout]
392
+
393
+ # Validate keys and values
394
+ timeout_hash.each do |key, val|
395
+ raise TypeError, "invalid timeout: :#{key}" unless default_timeouts.key?(key)
396
+
397
+ next if val.nil?
398
+
399
+ raise TypeError, ":#{key} must be numeric" unless val.is_a?(Numeric)
400
+ end
401
+
402
+ timeout_hash
377
403
  end
378
404
 
379
405
  def option_supported_compression_formats(value)
@@ -395,6 +421,20 @@ module HTTPX
395
421
  Array(value)
396
422
  end
397
423
 
424
+ # called after all options are initialized
425
+ def do_initialize
426
+ hs = @headers
427
+
428
+ # initialized default request headers
429
+ hs["user-agent"] = USER_AGENT unless hs.key?("user-agent")
430
+ hs["accept"] = "*/*" unless hs.key?("accept")
431
+ if hs.key?("range")
432
+ hs.delete("accept-encoding")
433
+ else
434
+ hs["accept-encoding"] = supported_compression_formats unless hs.key?("accept-encoding")
435
+ end
436
+ end
437
+
398
438
  def access_option(obj, k, ivar_map)
399
439
  case obj
400
440
  when Hash
@@ -404,6 +444,8 @@ module HTTPX
404
444
  end
405
445
  end
406
446
 
447
+ # rubocop:disable Lint/UselessConstantScoping
448
+ # these really need to be defined at the end of the class
407
449
  SET_TEMPORARY_NAME = ->(klass, pl = nil) do
408
450
  if klass.respond_to?(:set_temporary_name) # ruby 3.4 only
409
451
  name = klass.name || "#{klass.superclass.name}(plugin)"
@@ -458,6 +500,7 @@ module HTTPX
458
500
  :pool_options => EMPTY_HASH,
459
501
  :ip_families => nil,
460
502
  :close_on_fork => false,
461
- }.freeze
503
+ }.each_value(&:freeze).freeze
504
+ # rubocop:enable Lint/UselessConstantScoping
462
505
  end
463
506
  end