httpx 1.4.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7edc783221b5d919a1c788bf360f0cc2d43ee1556eb893c6b5d377d1bd3b4241
4
- data.tar.gz: 14dc4593ae5c46acb33a4f242a0058e75eddec70bd22fdf75c05496240de3abc
3
+ metadata.gz: b8a0ae955506767cc7b2f0a8134dd920bc937f88aeba2f72f691489e4d2199ab
4
+ data.tar.gz: 96503529f27fddf76b41250f3a7a0686c93d9d6c1091255275ed1d5b2beada11
5
5
  SHA512:
6
- metadata.gz: 181bc9e6155708c2d1cdaee3dc7c5c15cd5e2b1ba8d7911b6dd5a79e1cebe38558766ec89d8446483d434c7387e330874a70633b2ed5fb8f6a9d8a3401a4abf4
7
- data.tar.gz: 49f57c406a2a5c5be83c018c2151ef2e842c266e93d9609dff15684e662bb25580e7c84e2e96a32d1a45094c9baf8ad18f89dbab1e8235127f4d40429f733572
6
+ metadata.gz: d16170323b9f5d016f496e848be6b6fb50046402ca644c2a016d05fe1af4a5b045a400b31530c8fa38f85a61351125062edce01bd04c975bc45c3205545aef71
7
+ data.tar.gz: a800e8814449fcf2d3149aadc5f335f458f7dc7fa537d7c676d8460b6bfcfd1a9cc0a96f3350b9492846fea563fca15605f228ef539b39ef36985df05431d193
@@ -0,0 +1,19 @@
1
+ # 1.4.1
2
+
3
+ ## Bugfixes
4
+
5
+ * several `datadog` integration bugfixes
6
+ * only load the `datadog` integration when the `datadog` sdk is loaded (and not other gems that may define the `Datadog` module, like `dogstatsd`)
7
+ * do not trace if datadog integration is loaded but disabled
8
+ * distributed headers are now sent along (when the configuration is enabled, which it is by default)
9
+ * fix for handling multiple `GOAWAY` frames coming from the server (node.js servers seem to send multiple frames on connection timeout)
10
+ * fix regression for when a url is used with `httpx` which is not `http://` or `https://` (should raise `HTTPX::UnsupportedSchemaError`)
11
+ * worked around `IO.copy_stream` which was emitting incorrect bytes for HTTP/2 requests which bodies larger than the maximum supported frame size.
12
+ * multipart requests: make sure that a body declared as `Pathname` is opened for reading in binary mode.
13
+ * `webmock` integration: ensure that request events are emitted (such as plugins and integrations relying in it, such as `datadog` and the OTel integration)
14
+ * native resolver: do not propagate successful name resolutions for connections which were already closed.
15
+ * native resolver: fixed name resolution stalling, in a multi-request to multi-origin scenario, when a resolution timeout would happen.
16
+
17
+ ## Chore
18
+
19
+ * refactor of the happy eyeballs and connection coalescing logic to not rely on callbacks, and instead on instance variable management (makes code more straightforward to read).
@@ -27,62 +27,54 @@ module Datadog::Tracing
27
27
  # Enables tracing for httpx requests.
28
28
  #
29
29
  # A span will be created for each request transaction; the span is created lazily only when
30
- # receiving a response, and it is fed the start time stored inside the tracer object.
30
+ # buffering a request, and it is fed the start time stored inside the tracer object.
31
31
  #
32
32
  module Plugin
33
- class RequestTracer
34
- include Contrib::HttpAnnotationHelper
33
+ module RequestTracer
34
+ extend Contrib::HttpAnnotationHelper
35
+
36
+ module_function
35
37
 
36
38
  SPAN_REQUEST = "httpx.request"
37
39
 
38
- # initializes the tracer object on the +request+.
39
- def initialize(request)
40
- @request = request
41
- @start_time = nil
40
+ # initializes tracing on the +request+.
41
+ def call(request)
42
+ return unless configuration(request).enabled
43
+
44
+ span = nil
42
45
 
43
46
  # request objects are reused, when already buffered requests get rerouted to a different
44
47
  # connection due to connection issues, or when they already got a response, but need to
45
48
  # be retried. In such situations, the original span needs to be extended for the former,
46
49
  # while a new is required for the latter.
47
- request.on(:idle) { reset }
50
+ request.on(:idle) do
51
+ span = nil
52
+ end
48
53
  # the span is initialized when the request is buffered in the parser, which is the closest
49
54
  # one gets to actually sending the request.
50
- request.on(:headers) { call }
51
- end
52
-
53
- # sets up the span start time, while preparing the on response callback.
54
- def call(*args)
55
- return if @start_time
56
-
57
- start(*args)
55
+ request.on(:headers) do
56
+ next if span
58
57
 
59
- @request.once(:response, &method(:finish))
60
- end
58
+ span = initialize_span(request, now)
59
+ end
61
60
 
62
- private
61
+ request.on(:response) do |response|
62
+ unless span
63
+ next unless response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
63
64
 
64
- # just sets the span init time. It can be passed a +start_time+ in cases where
65
- # this is collected outside the request transaction.
66
- def start(start_time = now)
67
- @start_time = start_time
68
- end
65
+ # handles the case when the +error+ happened during name resolution, which means
66
+ # that the tracing start point hasn't been triggered yet; in such cases, the approximate
67
+ # initial resolving time is collected from the connection, and used as span start time,
68
+ # and the tracing object in inserted before the on response callback is called.
69
+ span = initialize_span(request, response.error.connection.init_time)
69
70
 
70
- # resets the start time for already finished request transactions.
71
- def reset
72
- return unless @start_time
71
+ end
73
72
 
74
- start
73
+ finish(response, span)
74
+ end
75
75
  end
76
76
 
77
- # creates the span from the collected +@start_time+ to what the +response+ state
78
- # contains. It also resets internal state to allow this object to be reused.
79
- def finish(response)
80
- return unless @start_time
81
-
82
- span = initialize_span
83
-
84
- return unless span
85
-
77
+ def finish(response, span)
86
78
  if response.is_a?(::HTTPX::ErrorResponse)
87
79
  span.set_error(response.error)
88
80
  else
@@ -92,40 +84,40 @@ module Datadog::Tracing
92
84
  end
93
85
 
94
86
  span.finish
95
- ensure
96
- @start_time = nil
97
87
  end
98
88
 
99
89
  # return a span initialized with the +@request+ state.
100
- def initialize_span
101
- verb = @request.verb
102
- uri = @request.uri
90
+ def initialize_span(request, start_time)
91
+ verb = request.verb
92
+ uri = request.uri
93
+
94
+ config = configuration(request)
103
95
 
104
- span = create_span(@request)
96
+ span = create_span(request, config, start_time)
105
97
 
106
98
  span.resource = verb
107
99
 
108
100
  # Add additional request specific tags to the span.
109
101
 
110
- span.set_tag(TAG_URL, @request.path)
102
+ span.set_tag(TAG_URL, request.path)
111
103
  span.set_tag(TAG_METHOD, verb)
112
104
 
113
105
  span.set_tag(TAG_TARGET_HOST, uri.host)
114
- span.set_tag(TAG_TARGET_PORT, uri.port.to_s)
106
+ span.set_tag(TAG_TARGET_PORT, uri.port)
115
107
 
116
108
  # Tag as an external peer service
117
109
  span.set_tag(TAG_PEER_SERVICE, span.service)
118
110
 
119
- if configuration[:distributed_tracing]
111
+ if config[:distributed_tracing]
120
112
  propagate_trace_http(
121
- Datadog::Tracing.active_trace.to_digest,
122
- @request.headers
113
+ Datadog::Tracing.active_trace,
114
+ request.headers
123
115
  )
124
116
  end
125
117
 
126
118
  # Set analytics sample rate
127
- if Contrib::Analytics.enabled?(configuration[:analytics_enabled])
128
- Contrib::Analytics.set_sample_rate(span, configuration[:analytics_sample_rate])
119
+ if Contrib::Analytics.enabled?(config[:analytics_enabled])
120
+ Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
129
121
  end
130
122
 
131
123
  span
@@ -138,34 +130,34 @@ module Datadog::Tracing
138
130
  ::Datadog::Core::Utils::Time.now.utc
139
131
  end
140
132
 
141
- def configuration
142
- @configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
133
+ def configuration(request)
134
+ Datadog.configuration.tracing[:httpx, request.uri.host]
143
135
  end
144
136
 
145
137
  if Gem::Version.new(DATADOG_VERSION::STRING) >= Gem::Version.new("2.0.0")
146
- def propagate_trace_http(digest, headers)
147
- Datadog::Tracing::Contrib::HTTP.inject(digest, headers)
138
+ def propagate_trace_http(trace, headers)
139
+ Datadog::Tracing::Contrib::HTTP.inject(trace, headers)
148
140
  end
149
141
 
150
- def create_span(request)
142
+ def create_span(request, configuration, start_time)
151
143
  Datadog::Tracing.trace(
152
144
  SPAN_REQUEST,
153
- service: service_name(request.uri.host, configuration, Datadog.configuration_for(self)),
145
+ service: service_name(request.uri.host, configuration),
154
146
  type: TYPE_OUTBOUND,
155
- start_time: @start_time
147
+ start_time: start_time
156
148
  )
157
149
  end
158
150
  else
159
- def propagate_trace_http(digest, headers)
160
- Datadog::Tracing::Propagation::HTTP.inject!(digest, headers)
151
+ def propagate_trace_http(trace, headers)
152
+ Datadog::Tracing::Propagation::HTTP.inject!(trace.to_digest, headers)
161
153
  end
162
154
 
163
- def create_span(request)
155
+ def create_span(request, configuration, start_time)
164
156
  Datadog::Tracing.trace(
165
157
  SPAN_REQUEST,
166
- service: service_name(request.uri.host, configuration, Datadog.configuration_for(self)),
158
+ service: service_name(request.uri.host, configuration),
167
159
  span_type: TYPE_OUTBOUND,
168
- start_time: @start_time
160
+ start_time: start_time
169
161
  )
170
162
  end
171
163
  end
@@ -178,7 +170,7 @@ module Datadog::Tracing
178
170
 
179
171
  return unless Datadog::Tracing.enabled?
180
172
 
181
- RequestTracer.new(self)
173
+ RequestTracer.call(self)
182
174
  end
183
175
  end
184
176
 
@@ -190,26 +182,6 @@ module Datadog::Tracing
190
182
 
191
183
  @init_time = ::Datadog::Core::Utils::Time.now.utc
192
184
  end
193
-
194
- # handles the case when the +error+ happened during name resolution, which meanns
195
- # that the tracing logic hasn't been injected yet; in such cases, the approximate
196
- # initial resolving time is collected from the connection, and used as span start time,
197
- # and the tracing object in inserted before the on response callback is called.
198
- def handle_error(error, request = nil)
199
- return super unless Datadog::Tracing.enabled?
200
-
201
- return super unless error.respond_to?(:connection)
202
-
203
- @pending.each do |req|
204
- next if request and request == req
205
-
206
- RequestTracer.new(req).call(error.connection.init_time)
207
- end
208
-
209
- RequestTracer.new(request).call(error.connection.init_time) if request
210
-
211
- super
212
- end
213
185
  end
214
186
  end
215
187
 
@@ -59,7 +59,9 @@ module WebMock
59
59
 
60
60
  connection.once(:unmock_connection) do
61
61
  unless connection.addresses
62
- connection.__send__(:callbacks)[:connect_error].clear
62
+ # reset Happy Eyeballs, fail early
63
+ connection.sibling = nil
64
+
63
65
  deselect_connection(connection, selector)
64
66
  end
65
67
  resolve_connection(connection, selector)
@@ -114,6 +116,10 @@ module WebMock
114
116
  response = Plugin.build_from_webmock_response(request, mock_response)
115
117
  WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
116
118
  log { "mocking #{request.uri} with #{mock_response.inspect}" }
119
+ request.transition(:headers)
120
+ request.transition(:body)
121
+ request.transition(:trailers)
122
+ request.transition(:done)
117
123
  request.response = response
118
124
  request.emit(:response, response)
119
125
  response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
@@ -137,7 +137,7 @@ module HTTPX
137
137
 
138
138
  emit(:error, req, ex)
139
139
  end
140
- @pending.each do |req|
140
+ while (req = @pending.shift)
141
141
  next if request && request == req
142
142
 
143
143
  emit(:error, req, ex)
@@ -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|
@@ -194,6 +196,12 @@ module HTTPX
194
196
  end
195
197
  end
196
198
 
199
+ def io_connected?
200
+ return @coalesced_connection.io_connected? if @coalesced_connection
201
+
202
+ @io && @io.state == :connected
203
+ end
204
+
197
205
  def connecting?
198
206
  @state == :idle
199
207
  end
@@ -342,6 +350,35 @@ module HTTPX
342
350
  on_error(error)
343
351
  end
344
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
+
345
382
  private
346
383
 
347
384
  def connect
@@ -531,20 +568,22 @@ module HTTPX
531
568
  @exhausted = true
532
569
  current_session = @current_session
533
570
  current_selector = @current_selector
534
- parser.close
535
- @pending.concat(parser.pending)
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
+
536
579
  case @state
537
580
  when :closed
538
581
  idling
539
582
  @exhausted = false
540
- @current_session = current_session
541
- @current_selector = current_selector
542
583
  when :closing
543
- once(:close) do
584
+ once(:closed) do
544
585
  idling
545
586
  @exhausted = false
546
- @current_session = current_session
547
- @current_selector = current_selector
548
587
  end
549
588
  end
550
589
  end
@@ -613,13 +652,13 @@ module HTTPX
613
652
  # connect errors, exit gracefully
614
653
  error = ConnectionError.new(e.message)
615
654
  error.set_backtrace(e.backtrace)
616
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
655
+ handle_connect_error(error) if connecting?
617
656
  @state = :closed
618
657
  disconnect
619
658
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
620
659
  # connect errors, exit gracefully
621
660
  handle_error(e)
622
- connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
661
+ handle_connect_error(e) if connecting?
623
662
  @state = :closed
624
663
  disconnect
625
664
  end
@@ -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
@@ -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
 
@@ -92,6 +94,8 @@ module HTTPX
92
94
  @uri = origin.merge("#{base_path}#{@uri}")
93
95
  end
94
96
 
97
+ raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
98
+
95
99
  @state = :idle
96
100
  @response = nil
97
101
  @peer_address = nil
@@ -263,6 +267,8 @@ module HTTPX
263
267
  return unless @state == :body
264
268
  when :done
265
269
  return if @state == :expect
270
+
271
+ @body.close
266
272
  end
267
273
  @state = nextstate
268
274
  emit(@state, self)
@@ -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)
@@ -63,7 +63,10 @@ module HTTPX
63
63
  @ns_index += 1
64
64
  nameserver = @nameserver
65
65
  if nameserver && @ns_index < nameserver.size
66
- log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
66
+ log do
67
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
68
+ "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
69
+ end
67
70
  transition(:idle)
68
71
  @timeouts.clear
69
72
  else
@@ -140,17 +143,22 @@ module HTTPX
140
143
 
141
144
  return unless timeout <= 0
142
145
 
146
+ elapsed_after = @_timeouts[@_timeouts.size - @timeouts[host].size]
143
147
  @timeouts[host].shift
144
148
 
145
149
  if !@timeouts[host].empty?
146
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
150
+ log do
151
+ "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{elapsed_after}s, retry (with #{@timeouts[host].first}s) #{host}..."
152
+ end
147
153
  # must downgrade to tcp AND retry on same host as last
148
154
  downgrade_socket
149
155
  resolve(connection, h)
150
156
  elsif @ns_index + 1 < @nameserver.size
151
157
  # try on the next nameserver
152
158
  @ns_index += 1
153
- log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
159
+ log do
160
+ "resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
161
+ end
154
162
  transition(:idle)
155
163
  @timeouts.clear
156
164
  resolve(connection, h)
@@ -167,7 +175,8 @@ module HTTPX
167
175
  ex = ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.peer.host}")
168
176
  ex.set_backtrace(ex ? ex.backtrace : caller)
169
177
  emit_resolve_error(connection, host, ex)
170
- emit(:close, self)
178
+
179
+ close_or_resolve
171
180
  end
172
181
  end
173
182
 
@@ -249,15 +258,15 @@ module HTTPX
249
258
  hostname, connection = @queries.first
250
259
  reset_hostname(hostname, reset_candidates: false)
251
260
 
252
- unless @queries.value?(connection)
261
+ if @queries.value?(connection)
262
+ resolve
263
+ else
253
264
  @connections.delete(connection)
254
265
  ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
255
266
  ex.set_backtrace(ex ? ex.backtrace : caller)
256
267
  emit_resolve_error(connection, connection.peer.host, ex)
257
- emit(:close, self)
268
+ close_or_resolve
258
269
  end
259
-
260
- resolve
261
270
  when :message_truncated
262
271
  # TODO: what to do if it's already tcp??
263
272
  return if @socket_type == :tcp
@@ -326,7 +335,7 @@ module HTTPX
326
335
  transition(:idle)
327
336
  transition(:open)
328
337
  end
329
- log { "resolver: ALIAS #{hostname_alias} for #{name}" }
338
+ log { "resolver #{FAMILY_TYPES[@record_type]}: ALIAS #{hostname_alias} for #{name}" }
330
339
  resolve(connection, hostname_alias)
331
340
  return
332
341
  end
@@ -338,9 +347,7 @@ module HTTPX
338
347
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
339
348
  end
340
349
  end
341
- return emit(:close, self) if @connections.empty?
342
-
343
- resolve
350
+ close_or_resolve
344
351
  end
345
352
 
346
353
  def resolve(connection = @connections.first, hostname = nil)
@@ -352,7 +359,10 @@ module HTTPX
352
359
 
353
360
  if hostname.nil?
354
361
  hostname = connection.peer.host
355
- log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
362
+ log do
363
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
364
+ "resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
365
+ end if connection.peer.non_ascii_hostname
356
366
 
357
367
  hostname = generate_candidates(hostname).each do |name|
358
368
  @queries[name] = connection
@@ -360,14 +370,14 @@ module HTTPX
360
370
  else
361
371
  @queries[hostname] = connection
362
372
  end
363
- log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
373
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
364
374
  begin
365
375
  @write_buffer << encode_dns_query(hostname)
366
376
  rescue Resolv::DNS::EncodeError => e
367
377
  reset_hostname(hostname, connection: connection)
368
378
  @connections.delete(connection)
369
379
  emit_resolve_error(connection, hostname, e)
370
- emit(:close, self) if @connections.empty?
380
+ close_or_resolve
371
381
  end
372
382
  end
373
383
 
@@ -397,10 +407,10 @@ module HTTPX
397
407
 
398
408
  case @socket_type
399
409
  when :udp
400
- log { "resolver: server: udp://#{ip}:#{port}..." }
410
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: udp://#{ip}:#{port}..." }
401
411
  UDP.new(ip, port, @options)
402
412
  when :tcp
403
- log { "resolver: server: tcp://#{ip}:#{port}..." }
413
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: tcp://#{ip}:#{port}..." }
404
414
  origin = URI("tcp://#{ip}:#{port}")
405
415
  TCP.new(origin, [ip], @options)
406
416
  end
@@ -463,7 +473,7 @@ module HTTPX
463
473
  emit_resolve_error(connection, host, error)
464
474
  end
465
475
  end
466
- emit(:close, self) if @connections.empty?
476
+ close_or_resolve
467
477
  end
468
478
 
469
479
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -478,5 +488,13 @@ module HTTPX
478
488
  # reset timeouts
479
489
  @timeouts.delete_if { |h, _| candidates.include?(h) }
480
490
  end
491
+
492
+ def close_or_resolve
493
+ if @connections.empty?
494
+ emit(:close, self)
495
+ else
496
+ resolve
497
+ end
498
+ end
481
499
  end
482
500
  end
@@ -72,17 +72,21 @@ module HTTPX
72
72
  # double emission check, but allow early resolution to work
73
73
  return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
74
74
 
75
- log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.peer.host}: #{addresses.inspect}" }
75
+ log do
76
+ "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
77
+ "answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.peer.host}: #{addresses.inspect}"
78
+ end
79
+
76
80
  if @current_selector && # if triggered by early resolve, session may not be here yet
77
81
  !connection.io &&
78
82
  connection.options.ip_families.size > 1 &&
79
83
  family == Socket::AF_INET &&
80
84
  addresses.first.to_s != connection.peer.host.to_s
81
- log { "resolver: A response, applying resolution delay..." }
85
+ log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
86
+
82
87
  @current_selector.after(0.05) do
83
- unless connection.state == :closed ||
84
- # double emission check
85
- (connection.addresses && addresses.intersect?(connection.addresses))
88
+ # double emission check
89
+ unless connection.addresses && addresses.intersect?(connection.addresses)
86
90
  emit_resolved_connection(connection, addresses, early_resolve)
87
91
  end
88
92
  end
@@ -97,6 +101,8 @@ module HTTPX
97
101
  begin
98
102
  connection.addresses = addresses
99
103
 
104
+ return if connection.state == :closed
105
+
100
106
  emit(:resolve, connection)
101
107
  rescue StandardError => e
102
108
  if early_resolve
@@ -146,7 +152,7 @@ module HTTPX
146
152
  end
147
153
 
148
154
  def emit_connection_error(connection, error)
149
- return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
155
+ return connection.handle_connect_error(error) if connection.connecting?
150
156
 
151
157
  connection.emit(:error, error)
152
158
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "resolv"
5
4
 
6
5
  module HTTPX
7
6
  class Resolver::System < Resolver::Resolver
8
7
  using URIExtensions
9
- extend Forwardable
10
8
 
11
9
  RESOLV_ERRORS = [Resolv::ResolvError,
12
10
  Resolv::DNS::Requester::RequestError,
@@ -24,8 +22,6 @@ module HTTPX
24
22
 
25
23
  attr_reader :state
26
24
 
27
- def_delegator :@connections, :empty?
28
-
29
25
  def initialize(options)
30
26
  super(nil, options)
31
27
  @resolver_options = @options.resolver_options
@@ -162,7 +158,9 @@ module HTTPX
162
158
 
163
159
  hostname = connection.peer.host
164
160
  scheme = connection.origin.scheme
165
- log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
161
+ log do
162
+ "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
163
+ end if connection.peer.non_ascii_hostname
166
164
 
167
165
  transition(:open)
168
166
 
@@ -92,10 +92,6 @@ module HTTPX
92
92
  end
93
93
  end
94
94
 
95
- def empty?
96
- @selectables.empty?
97
- end
98
-
99
95
  # deregisters +io+ from selectables.
100
96
  def deregister(io)
101
97
  @selectables.delete(io)
data/lib/httpx/session.rb CHANGED
@@ -69,8 +69,6 @@ module HTTPX
69
69
  while (connection = @pool.pop_connection)
70
70
  next if connection.state == :closed
71
71
 
72
- connection.current_session = self
73
- connection.current_selector = selector
74
72
  select_connection(connection, selector)
75
73
  end
76
74
  begin
@@ -126,9 +124,15 @@ module HTTPX
126
124
  end
127
125
 
128
126
  def select_connection(connection, selector)
127
+ pin_connection(connection, selector)
129
128
  selector.register(connection)
130
129
  end
131
130
 
131
+ def pin_connection(connection, selector)
132
+ connection.current_session = self
133
+ connection.current_selector = selector
134
+ end
135
+
132
136
  alias_method :select_resolver, :select_connection
133
137
 
134
138
  def deselect_connection(connection, selector, cloned = false)
@@ -160,36 +164,8 @@ module HTTPX
160
164
  new_connection = connection.class.new(connection.origin, connection.options)
161
165
 
162
166
  new_connection.family = family
163
- new_connection.current_session = self
164
- new_connection.current_selector = selector
165
-
166
- connection.once(:tcp_open) { new_connection.force_reset(true) }
167
- connection.once(:connect_error) do |err|
168
- if new_connection.connecting?
169
- new_connection.merge(connection)
170
- connection.emit(:cloned, new_connection)
171
- connection.force_reset(true)
172
- else
173
- connection.__send__(:handle_error, err)
174
- end
175
- end
176
167
 
177
- new_connection.once(:tcp_open) do |new_conn|
178
- if new_conn != connection
179
- new_conn.merge(connection)
180
- connection.force_reset(true)
181
- end
182
- end
183
- new_connection.once(:connect_error) do |err|
184
- if connection.connecting?
185
- # main connection has the requests
186
- connection.merge(new_connection)
187
- new_connection.emit(:cloned, connection)
188
- new_connection.force_reset(true)
189
- else
190
- new_connection.__send__(:handle_error, err)
191
- end
192
- end
168
+ connection.sibling = new_connection
193
169
 
194
170
  do_init_connection(new_connection, selector)
195
171
  new_connection
@@ -203,14 +179,15 @@ module HTTPX
203
179
 
204
180
  connection = @pool.checkout_connection(request_uri, options)
205
181
 
206
- connection.current_session = self
207
- connection.current_selector = selector
208
-
209
182
  case connection.state
210
183
  when :idle
211
184
  do_init_connection(connection, selector)
212
185
  when :open
213
- select_connection(connection, selector) if options.io
186
+ if options.io
187
+ select_connection(connection, selector)
188
+ else
189
+ pin_connection(connection, selector)
190
+ end
214
191
  when :closed
215
192
  connection.idling
216
193
  select_connection(connection, selector)
@@ -219,6 +196,8 @@ module HTTPX
219
196
  connection.idling
220
197
  select_connection(connection, selector)
221
198
  end
199
+ else
200
+ pin_connection(connection, selector)
222
201
  end
223
202
 
224
203
  connection
@@ -372,7 +351,6 @@ module HTTPX
372
351
  # resolve a name (not the same as name being an IP, yet)
373
352
  # 2. when the connection is initialized with an external already open IO.
374
353
  #
375
- connection.once(:connect_error, &connection.method(:handle_error))
376
354
  on_resolver_connection(connection, selector)
377
355
  return
378
356
  end
@@ -428,8 +406,6 @@ module HTTPX
428
406
  return false
429
407
  end
430
408
 
431
- conn2.emit(:tcp_open, conn1)
432
- conn1.merge(conn2)
433
409
  conn2.coalesced_connection = conn1
434
410
  select_connection(conn1, selector) if from_pool
435
411
  deselect_connection(conn2, selector)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "delegate"
4
4
 
5
5
  module HTTPX::Transcoder
6
6
  module Body
@@ -8,48 +8,32 @@ module HTTPX::Transcoder
8
8
 
9
9
  module_function
10
10
 
11
- class Encoder
12
- extend Forwardable
13
-
14
- def_delegator :@raw, :to_s
15
-
16
- def_delegator :@raw, :==
17
-
11
+ class Encoder < SimpleDelegator
18
12
  def initialize(body)
19
- @raw = body
13
+ body = body.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && body.is_a?(Pathname)
14
+ @body = body
15
+ super(body)
20
16
  end
21
17
 
22
18
  def bytesize
23
- if @raw.respond_to?(:bytesize)
24
- @raw.bytesize
25
- elsif @raw.respond_to?(:to_ary)
26
- @raw.sum(&:bytesize)
27
- elsif @raw.respond_to?(:size)
28
- @raw.size || Float::INFINITY
29
- elsif @raw.respond_to?(:length)
30
- @raw.length || Float::INFINITY
31
- elsif @raw.respond_to?(:each)
19
+ if @body.respond_to?(:bytesize)
20
+ @body.bytesize
21
+ elsif @body.respond_to?(:to_ary)
22
+ @body.sum(&:bytesize)
23
+ elsif @body.respond_to?(:size)
24
+ @body.size || Float::INFINITY
25
+ elsif @body.respond_to?(:length)
26
+ @body.length || Float::INFINITY
27
+ elsif @body.respond_to?(:each)
32
28
  Float::INFINITY
33
29
  else
34
- raise Error, "cannot determine size of body: #{@raw.inspect}"
30
+ raise Error, "cannot determine size of body: #{@body.inspect}"
35
31
  end
36
32
  end
37
33
 
38
34
  def content_type
39
35
  "application/octet-stream"
40
36
  end
41
-
42
- private
43
-
44
- def respond_to_missing?(meth, *args)
45
- @raw.respond_to?(meth, *args) || super
46
- end
47
-
48
- def method_missing(meth, *args, &block)
49
- return super unless @raw.respond_to?(meth)
50
-
51
- @raw.__send__(meth, *args, &block)
52
- end
53
37
  end
54
38
 
55
39
  def encode(body)
@@ -19,7 +19,7 @@ module HTTPX
19
19
  value = value[:body]
20
20
  end
21
21
 
22
- value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
22
+ value = value.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
23
23
 
24
24
  if value.respond_to?(:path) && value.respond_to?(:read)
25
25
  # either a File, a Tempfile, or something else which has to quack like a file
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.4.0"
4
+ VERSION = "1.4.1"
5
5
  end
data/lib/httpx.rb CHANGED
@@ -61,6 +61,6 @@ require "httpx/session_extensions"
61
61
 
62
62
  # load integrations when possible
63
63
 
64
- require "httpx/adapters/datadog" if defined?(DDTrace) || defined?(Datadog)
64
+ require "httpx/adapters/datadog" if defined?(DDTrace) || defined?(Datadog::Tracing)
65
65
  require "httpx/adapters/sentry" if defined?(Sentry)
66
66
  require "httpx/adapters/webmock" if defined?(WebMock)
data/sig/connection.rbs CHANGED
@@ -26,11 +26,12 @@ module HTTPX
26
26
  attr_reader pending: Array[Request]
27
27
  attr_reader options: Options
28
28
  attr_reader ssl_session: OpenSSL::SSL::Session?
29
+ attr_reader sibling: instance?
29
30
  attr_writer current_selector: Selector?
30
- attr_writer coalesced_connection: instance?
31
31
  attr_accessor current_session: Session?
32
32
  attr_accessor family: Integer?
33
33
 
34
+
34
35
  @window_size: Integer
35
36
  @read_buffer: Buffer
36
37
  @write_buffer: Buffer
@@ -45,6 +46,10 @@ module HTTPX
45
46
  @intervals: Array[Timers::Interval]
46
47
  @exhausted: bool
47
48
  @cloned: bool
49
+ @coalesced_connection: instance?
50
+ @sibling: instance?
51
+ @main_sibling: bool
52
+
48
53
 
49
54
  def addresses: () -> Array[ipaddr]?
50
55
 
@@ -70,6 +75,8 @@ module HTTPX
70
75
 
71
76
  def connecting?: () -> bool
72
77
 
78
+ def io_connected?: () -> bool
79
+
73
80
  def inflight?: () -> boolish
74
81
 
75
82
  def interests: () -> io_interests?
@@ -98,6 +105,12 @@ module HTTPX
98
105
 
99
106
  def handle_socket_timeout: (Numeric interval) -> void
100
107
 
108
+ def coalesced_connection=: (instance connection) -> void
109
+
110
+ def sibling=: (instance? connection) -> void
111
+
112
+ def handle_connect_error: (StandardError error) -> void
113
+
101
114
  private
102
115
 
103
116
  def initialize: (http_uri uri, Options options) -> void
@@ -134,6 +147,8 @@ module HTTPX
134
147
 
135
148
  def handle_error: (StandardError error, ?Request? request) -> void
136
149
 
150
+ def close_sibling: () -> void
151
+
137
152
  def purge_after_closed: () -> void
138
153
 
139
154
  def set_request_timeouts: (Request request) -> void
data/sig/request/body.rbs CHANGED
@@ -31,12 +31,4 @@ module HTTPX
31
31
 
32
32
  def self.initialize_deflater_body: (body_encoder body, Encoding | String encoding) -> body_encoder
33
33
  end
34
-
35
- class ProcIO
36
- @block: ^(String) -> void
37
-
38
- def initialize: (^(String) -> void) -> untyped
39
-
40
- def write: (String data) -> Integer
41
- end
42
34
  end
@@ -64,6 +64,8 @@ module HTTPX
64
64
  def handle_error: (NativeResolveError | StandardError) -> void
65
65
 
66
66
  def reset_hostname: (String hostname, ?connection: Connection, ?reset_candidates: bool) -> void
67
+
68
+ def close_or_resolve: () -> void
67
69
  end
68
70
  end
69
71
  end
data/sig/session.rbs CHANGED
@@ -25,6 +25,8 @@ module HTTPX
25
25
 
26
26
  def select_connection: (Connection connection, Selector selector) -> void
27
27
 
28
+ def pin_connection: (Resolver::Resolver | Connection connection, Selector selector) -> void
29
+
28
30
  def deselect_connection: (Connection connection, Selector selector, ?bool cloned) -> void
29
31
 
30
32
  def select_resolver: (Resolver::Native | Resolver::HTTPS resolver, Selector selector) -> void
@@ -4,9 +4,7 @@ module HTTPX
4
4
  class Error < HTTPX::Error
5
5
  end
6
6
 
7
- class Encoder
8
- extend Forwardable
9
-
7
+ class Encoder # < SimpleDelegator
10
8
  @raw: Object & bodyIO
11
9
 
12
10
  def bytesize: () -> (Integer | Float)
@@ -7,7 +7,7 @@ module HTTPX
7
7
 
8
8
  def bytesize: () -> (Integer | Float)
9
9
 
10
- def read: (?int? length, ?string outbuf) -> String?
10
+ def read: (?int? length, ?string? outbuf) -> String?
11
11
 
12
12
  def close: () -> void
13
13
  end
@@ -14,7 +14,7 @@ module HTTPX
14
14
 
15
15
  def bytesize: () -> (Integer | Float)
16
16
 
17
- def read: (?int? length, ?string outbuf) -> String?
17
+ def read: (?int? length, ?string? outbuf) -> String?
18
18
 
19
19
  def close: () -> void
20
20
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-18 00:00:00.000000000 Z
11
+ date: 2025-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -150,6 +150,7 @@ extra_rdoc_files:
150
150
  - doc/release_notes/1_3_3.md
151
151
  - doc/release_notes/1_3_4.md
152
152
  - doc/release_notes/1_4_0.md
153
+ - doc/release_notes/1_4_1.md
153
154
  files:
154
155
  - LICENSE.txt
155
156
  - README.md
@@ -271,6 +272,7 @@ files:
271
272
  - doc/release_notes/1_3_3.md
272
273
  - doc/release_notes/1_3_4.md
273
274
  - doc/release_notes/1_4_0.md
275
+ - doc/release_notes/1_4_1.md
274
276
  - lib/httpx.rb
275
277
  - lib/httpx/adapters/datadog.rb
276
278
  - lib/httpx/adapters/faraday.rb