httpx 1.4.0 → 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.
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