httpx 1.4.1 → 1.4.2
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 +4 -4
- data/README.md +1 -2
- data/doc/release_notes/1_4_2.md +20 -0
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/callbacks.rb +2 -2
- data/lib/httpx/connection/http2.rb +1 -1
- data/lib/httpx/connection.rb +32 -35
- data/lib/httpx/errors.rb +3 -0
- data/lib/httpx/loggable.rb +8 -1
- data/lib/httpx/plugins/callbacks.rb +1 -0
- data/lib/httpx/plugins/circuit_breaker.rb +1 -0
- data/lib/httpx/plugins/expect.rb +1 -1
- data/lib/httpx/request.rb +14 -0
- data/lib/httpx/resolver/native.rb +86 -48
- data/lib/httpx/resolver/resolver.rb +7 -6
- data/lib/httpx/selector.rb +33 -19
- data/lib/httpx/session.rb +3 -5
- data/lib/httpx/timers.rb +16 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/callbacks.rbs +2 -2
- data/sig/connection.rbs +3 -4
- data/sig/errors.rbs +3 -0
- data/sig/request.rbs +3 -0
- data/sig/resolver/native.rbs +4 -1
- data/sig/selector.rbs +1 -0
- data/sig/timers.rbs +15 -4
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3be0d06eb9b669cc4a1ba0818d39c76bdd4cdf41e496cb82efbb83b7c6c47241
|
4
|
+
data.tar.gz: bf778691f222095e080c83b16f71b3d9c8065db9f98f35498eeea317f013a686
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26045db79f85f7a136771bdca1852d93be6ad9c84a4285d26c7a97543e0209588fdc423276ea593c9ad26db960ec81a444326a01223e24a932cca8318f08e4ab
|
7
|
+
data.tar.gz: c280576f30f55590336c70c93ea63a4be1e9a8ac238318417b9c4227889518c04f2545d70c0dce74c4026465ad9a7e68f0a05ac6d9728a1035e8ba5c094036fc
|
data/README.md
CHANGED
@@ -157,7 +157,6 @@ All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
|
|
157
157
|
|
158
158
|
* Discuss your contribution in an issue
|
159
159
|
* Fork it
|
160
|
-
* Make your changes, add some tests
|
161
|
-
* Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
|
160
|
+
* Make your changes, add some tests (follow the instructions from [here](test/README.md))
|
162
161
|
* Open a Merge Request (that's Pull Request in Github-ish)
|
163
162
|
* Wait for feedback
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# 1.4.2
|
2
|
+
|
3
|
+
## Bugfixes
|
4
|
+
|
5
|
+
* faraday: use default reason when none is matched by Net::HTTP::STATUS_CODES
|
6
|
+
* native resolver: keep sending DNS queries if the socket is available, to avoid busy loops on select
|
7
|
+
* native resolver fixes for Happy Eyeballs v2
|
8
|
+
* do not apply resolution delay if the IPv4 IP was not resolved via DNS
|
9
|
+
* ignore ALIAS if DNS response carries IP answers
|
10
|
+
* do not try to query for names already awaiting answer from the resolver
|
11
|
+
* make sure all types of errors are propagated to connections
|
12
|
+
* make sure next candidate is picked up if receiving NX_DOMAIN_NOT_FOUND error from resolver
|
13
|
+
* raise error happening before any request is flushed to respective connections (avoids loop on non-actionable selector termination).
|
14
|
+
* fix "NoMethodError: undefined method `after' for nil:NilClass", happening for requests flushed into persistent connections which errored, and were retried in a different connection before triggering the timeout callbacks from the previously-closed connection.
|
15
|
+
|
16
|
+
|
17
|
+
## Chore
|
18
|
+
|
19
|
+
* Refactor of timers to allow for explicit and more performant single timer interval cancellation.
|
20
|
+
* default log message restructured to include info about process, thread and caller.
|
data/lib/httpx/callbacks.rb
CHANGED
@@ -4,7 +4,7 @@ module HTTPX
|
|
4
4
|
module Callbacks
|
5
5
|
def on(type, &action)
|
6
6
|
callbacks(type) << action
|
7
|
-
|
7
|
+
action
|
8
8
|
end
|
9
9
|
|
10
10
|
def once(type, &block)
|
@@ -12,10 +12,10 @@ module HTTPX
|
|
12
12
|
block.call(*args, &callback)
|
13
13
|
:delete
|
14
14
|
end
|
15
|
-
self
|
16
15
|
end
|
17
16
|
|
18
17
|
def emit(type, *args)
|
18
|
+
log { "emit #{type.inspect} callbacks" } if respond_to?(:log)
|
19
19
|
callbacks(type).delete_if { |pr| :delete == pr.call(*args) } # rubocop:disable Style/YodaCondition
|
20
20
|
end
|
21
21
|
|
@@ -125,7 +125,7 @@ module HTTPX
|
|
125
125
|
end
|
126
126
|
|
127
127
|
def handle_error(ex, request = nil)
|
128
|
-
if ex.
|
128
|
+
if ex.is_a?(OperationTimeoutError) && !@handshake_completed && @connection.state != :closed
|
129
129
|
@connection.goaway(:settings_timeout, "closing due to settings timeout")
|
130
130
|
emit(:close_handshake)
|
131
131
|
settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
|
data/lib/httpx/connection.rb
CHANGED
@@ -101,8 +101,6 @@ module HTTPX
|
|
101
101
|
@inflight = 0
|
102
102
|
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
103
103
|
|
104
|
-
@intervals = []
|
105
|
-
|
106
104
|
self.addresses = @options.addresses if @options.addresses
|
107
105
|
end
|
108
106
|
|
@@ -337,15 +335,7 @@ module HTTPX
|
|
337
335
|
end
|
338
336
|
|
339
337
|
def handle_socket_timeout(interval)
|
340
|
-
|
341
|
-
|
342
|
-
unless @intervals.empty?
|
343
|
-
# remove the intervals which will elapse
|
344
|
-
|
345
|
-
return
|
346
|
-
end
|
347
|
-
|
348
|
-
error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
|
338
|
+
error = OperationTimeoutError.new(interval, "timed out while waiting on select")
|
349
339
|
error.set_backtrace(caller)
|
350
340
|
on_error(error)
|
351
341
|
end
|
@@ -379,18 +369,20 @@ module HTTPX
|
|
379
369
|
force_reset(true)
|
380
370
|
end
|
381
371
|
|
382
|
-
private
|
383
|
-
|
384
|
-
def connect
|
385
|
-
transition(:open)
|
386
|
-
end
|
387
|
-
|
388
372
|
def disconnect
|
373
|
+
return unless @current_session && @current_selector
|
374
|
+
|
389
375
|
emit(:close)
|
390
376
|
@current_session = nil
|
391
377
|
@current_selector = nil
|
392
378
|
end
|
393
379
|
|
380
|
+
private
|
381
|
+
|
382
|
+
def connect
|
383
|
+
transition(:open)
|
384
|
+
end
|
385
|
+
|
394
386
|
def consume
|
395
387
|
return unless @io
|
396
388
|
|
@@ -485,6 +477,8 @@ module HTTPX
|
|
485
477
|
end
|
486
478
|
log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
|
487
479
|
unless siz
|
480
|
+
@write_buffer.clear
|
481
|
+
|
488
482
|
ex = EOFError.new("descriptor closed")
|
489
483
|
ex.set_backtrace(caller)
|
490
484
|
on_error(ex)
|
@@ -628,11 +622,15 @@ module HTTPX
|
|
628
622
|
other_connection.merge(self)
|
629
623
|
request.transition(:idle)
|
630
624
|
other_connection.send(request)
|
631
|
-
|
632
|
-
|
633
|
-
request
|
634
|
-
request.
|
625
|
+
next
|
626
|
+
when OperationTimeoutError
|
627
|
+
# request level timeouts should take precedence
|
628
|
+
next unless request.active_timeouts.empty?
|
635
629
|
end
|
630
|
+
|
631
|
+
response = ErrorResponse.new(request, ex)
|
632
|
+
request.response = response
|
633
|
+
request.emit(:response, response)
|
636
634
|
end
|
637
635
|
end
|
638
636
|
|
@@ -654,12 +652,14 @@ module HTTPX
|
|
654
652
|
error.set_backtrace(e.backtrace)
|
655
653
|
handle_connect_error(error) if connecting?
|
656
654
|
@state = :closed
|
655
|
+
purge_after_closed
|
657
656
|
disconnect
|
658
657
|
rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
|
659
658
|
# connect errors, exit gracefully
|
660
659
|
handle_error(e)
|
661
660
|
handle_connect_error(e) if connecting?
|
662
661
|
@state = :closed
|
662
|
+
purge_after_closed
|
663
663
|
disconnect
|
664
664
|
end
|
665
665
|
|
@@ -812,7 +812,7 @@ module HTTPX
|
|
812
812
|
end
|
813
813
|
|
814
814
|
def on_error(error, request = nil)
|
815
|
-
if error.
|
815
|
+
if error.is_a?(OperationTimeoutError)
|
816
816
|
|
817
817
|
# inactive connections do not contribute to the select loop, therefore
|
818
818
|
# they should not fail due to such errors.
|
@@ -857,7 +857,7 @@ module HTTPX
|
|
857
857
|
|
858
858
|
return if read_timeout.nil? || read_timeout.infinite?
|
859
859
|
|
860
|
-
set_request_timeout(request, read_timeout, :done, :response) do
|
860
|
+
set_request_timeout(:read_timeout, request, read_timeout, :done, :response) do
|
861
861
|
read_timeout_callback(request, read_timeout)
|
862
862
|
end
|
863
863
|
end
|
@@ -867,7 +867,7 @@ module HTTPX
|
|
867
867
|
|
868
868
|
return if write_timeout.nil? || write_timeout.infinite?
|
869
869
|
|
870
|
-
set_request_timeout(request, write_timeout, :headers, %i[done response]) do
|
870
|
+
set_request_timeout(:write_timeout, request, write_timeout, :headers, %i[done response]) do
|
871
871
|
write_timeout_callback(request, write_timeout)
|
872
872
|
end
|
873
873
|
end
|
@@ -877,7 +877,7 @@ module HTTPX
|
|
877
877
|
|
878
878
|
return if request_timeout.nil? || request_timeout.infinite?
|
879
879
|
|
880
|
-
set_request_timeout(request, request_timeout, :headers, :complete) do
|
880
|
+
set_request_timeout(:request_timeout, request, request_timeout, :headers, :complete) do
|
881
881
|
read_timeout_callback(request, request_timeout, RequestTimeoutError)
|
882
882
|
end
|
883
883
|
end
|
@@ -902,21 +902,18 @@ module HTTPX
|
|
902
902
|
on_error(error, request)
|
903
903
|
end
|
904
904
|
|
905
|
-
def set_request_timeout(request, timeout, start_event, finish_events, &callback)
|
906
|
-
request.
|
907
|
-
|
905
|
+
def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
|
906
|
+
request.set_timeout_callback(start_event) do
|
907
|
+
timer = @current_selector.after(timeout, callback)
|
908
|
+
request.active_timeouts << label
|
908
909
|
|
909
910
|
Array(finish_events).each do |event|
|
910
911
|
# clean up request timeouts if the connection errors out
|
911
|
-
request.
|
912
|
-
|
913
|
-
|
914
|
-
@intervals.delete(interval) if interval.no_callbacks?
|
915
|
-
end
|
912
|
+
request.set_timeout_callback(event) do
|
913
|
+
timer.cancel
|
914
|
+
request.active_timeouts.delete(label)
|
916
915
|
end
|
917
916
|
end
|
918
|
-
|
919
|
-
@intervals << interval
|
920
917
|
end
|
921
918
|
end
|
922
919
|
|
data/lib/httpx/errors.rb
CHANGED
@@ -77,6 +77,9 @@ module HTTPX
|
|
77
77
|
# Error raised when there was a timeout while resolving a domain to an IP.
|
78
78
|
class ResolveTimeoutError < TimeoutError; end
|
79
79
|
|
80
|
+
# Error raise when there was a timeout waiting for readiness of the socket the request is related to.
|
81
|
+
class OperationTimeoutError < TimeoutError; end
|
82
|
+
|
80
83
|
# Error raised when there was an error while resolving a domain to an IP.
|
81
84
|
class ResolveError < Error; end
|
82
85
|
|
data/lib/httpx/loggable.rb
CHANGED
@@ -22,7 +22,14 @@ module HTTPX
|
|
22
22
|
|
23
23
|
return unless debug_stream
|
24
24
|
|
25
|
-
|
25
|
+
klass = self.class
|
26
|
+
|
27
|
+
until (class_name = klass.name)
|
28
|
+
klass = klass.superclass
|
29
|
+
end
|
30
|
+
|
31
|
+
message = +"(pid:#{Process.pid} tid:#{Thread.current.object_id}, self:#{class_name}##{object_id}) "
|
32
|
+
message << msg.call << "\n"
|
26
33
|
message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
|
27
34
|
debug_stream << message
|
28
35
|
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -84,7 +84,7 @@ module HTTPX
|
|
84
84
|
|
85
85
|
return if expect_timeout.nil? || expect_timeout.infinite?
|
86
86
|
|
87
|
-
set_request_timeout(request, expect_timeout, :expect, %i[body response]) do
|
87
|
+
set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
|
88
88
|
# expect timeout expired
|
89
89
|
if request.state == :expect && !request.expects?
|
90
90
|
Expect.no_expect_store << request.origin
|
data/lib/httpx/request.rb
CHANGED
@@ -45,6 +45,8 @@ module HTTPX
|
|
45
45
|
|
46
46
|
attr_writer :persistent
|
47
47
|
|
48
|
+
attr_reader :active_timeouts
|
49
|
+
|
48
50
|
# will be +true+ when request body has been completely flushed.
|
49
51
|
def_delegator :@body, :empty?
|
50
52
|
|
@@ -100,6 +102,7 @@ module HTTPX
|
|
100
102
|
@response = nil
|
101
103
|
@peer_address = nil
|
102
104
|
@persistent = @options.persistent
|
105
|
+
@active_timeouts = []
|
103
106
|
end
|
104
107
|
|
105
108
|
# the read timeout defined for this requet.
|
@@ -245,8 +248,10 @@ module HTTPX
|
|
245
248
|
@body.rewind
|
246
249
|
@response = nil
|
247
250
|
@drainer = nil
|
251
|
+
@active_timeouts.clear
|
248
252
|
when :headers
|
249
253
|
return unless @state == :idle
|
254
|
+
|
250
255
|
when :body
|
251
256
|
return unless @state == :headers ||
|
252
257
|
@state == :expect
|
@@ -279,6 +284,15 @@ module HTTPX
|
|
279
284
|
def expects?
|
280
285
|
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
281
286
|
end
|
287
|
+
|
288
|
+
def set_timeout_callback(event, &callback)
|
289
|
+
clb = once(event, &callback)
|
290
|
+
|
291
|
+
# reset timeout callbacks when requests get rerouted to a different connection
|
292
|
+
once(:idle) do
|
293
|
+
callbacks(event).delete(clb)
|
294
|
+
end
|
295
|
+
end
|
282
296
|
end
|
283
297
|
end
|
284
298
|
|
@@ -35,6 +35,7 @@ module HTTPX
|
|
35
35
|
@_timeouts = Array(@resolver_options[:timeouts])
|
36
36
|
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
37
37
|
@connections = []
|
38
|
+
@name = nil
|
38
39
|
@queries = {}
|
39
40
|
@read_buffer = "".b
|
40
41
|
@write_buffer = Buffer.new(@resolver_options[:packet_size])
|
@@ -58,22 +59,6 @@ module HTTPX
|
|
58
59
|
when :open
|
59
60
|
consume
|
60
61
|
end
|
61
|
-
nil
|
62
|
-
rescue Errno::EHOSTUNREACH => e
|
63
|
-
@ns_index += 1
|
64
|
-
nameserver = @nameserver
|
65
|
-
if nameserver && @ns_index < nameserver.size
|
66
|
-
log do
|
67
|
-
"resolver #{FAMILY_TYPES[@record_type]}: " \
|
68
|
-
"failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
|
69
|
-
end
|
70
|
-
transition(:idle)
|
71
|
-
@timeouts.clear
|
72
|
-
else
|
73
|
-
handle_error(e)
|
74
|
-
end
|
75
|
-
rescue NativeResolveError => e
|
76
|
-
handle_error(e)
|
77
62
|
end
|
78
63
|
|
79
64
|
def interests
|
@@ -108,9 +93,7 @@ module HTTPX
|
|
108
93
|
@timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
|
109
94
|
end
|
110
95
|
|
111
|
-
def handle_socket_timeout(interval)
|
112
|
-
do_retry(interval)
|
113
|
-
end
|
96
|
+
def handle_socket_timeout(interval); end
|
114
97
|
|
115
98
|
private
|
116
99
|
|
@@ -123,32 +106,60 @@ module HTTPX
|
|
123
106
|
end
|
124
107
|
|
125
108
|
def consume
|
126
|
-
|
127
|
-
|
128
|
-
|
109
|
+
loop do
|
110
|
+
dread if calculate_interests == :r
|
111
|
+
|
112
|
+
break unless calculate_interests == :w
|
113
|
+
|
114
|
+
# do_retry
|
115
|
+
dwrite
|
116
|
+
|
117
|
+
break unless calculate_interests == :r
|
118
|
+
end
|
119
|
+
rescue Errno::EHOSTUNREACH => e
|
120
|
+
@ns_index += 1
|
121
|
+
nameserver = @nameserver
|
122
|
+
if nameserver && @ns_index < nameserver.size
|
123
|
+
log do
|
124
|
+
"resolver #{FAMILY_TYPES[@record_type]}: " \
|
125
|
+
"failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
|
126
|
+
end
|
127
|
+
transition(:idle)
|
128
|
+
@timeouts.clear
|
129
|
+
retry
|
130
|
+
else
|
131
|
+
handle_error(e)
|
132
|
+
emit(:close, self)
|
133
|
+
end
|
134
|
+
rescue NativeResolveError => e
|
135
|
+
handle_error(e)
|
136
|
+
close_or_resolve
|
137
|
+
retry unless closed?
|
129
138
|
end
|
130
139
|
|
131
|
-
def
|
132
|
-
|
140
|
+
def schedule_retry
|
141
|
+
h = @name
|
133
142
|
|
134
|
-
|
143
|
+
return unless h
|
135
144
|
|
136
|
-
|
145
|
+
connection = @queries[h]
|
137
146
|
|
138
|
-
|
147
|
+
timeouts = @timeouts[h]
|
148
|
+
timeout = timeouts.shift
|
139
149
|
|
140
|
-
|
141
|
-
|
142
|
-
timeout = (@timeouts[host][0] -= loop_time)
|
150
|
+
@timer = @current_selector.after(timeout) do
|
151
|
+
next unless @connections.include?(connection)
|
143
152
|
|
144
|
-
|
153
|
+
do_retry(h, connection, timeout)
|
154
|
+
end
|
155
|
+
end
|
145
156
|
|
146
|
-
|
147
|
-
@timeouts[
|
157
|
+
def do_retry(h, connection, interval)
|
158
|
+
timeouts = @timeouts[h]
|
148
159
|
|
149
|
-
if
|
160
|
+
if !timeouts.empty?
|
150
161
|
log do
|
151
|
-
"resolver #{FAMILY_TYPES[@record_type]}: timeout after #{
|
162
|
+
"resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..."
|
152
163
|
end
|
153
164
|
# must downgrade to tcp AND retry on same host as last
|
154
165
|
downgrade_socket
|
@@ -157,22 +168,28 @@ module HTTPX
|
|
157
168
|
# try on the next nameserver
|
158
169
|
@ns_index += 1
|
159
170
|
log do
|
160
|
-
"resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{
|
171
|
+
"resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{h} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
|
161
172
|
end
|
162
173
|
transition(:idle)
|
163
174
|
@timeouts.clear
|
164
175
|
resolve(connection, h)
|
165
176
|
else
|
166
177
|
|
167
|
-
@timeouts.delete(
|
178
|
+
@timeouts.delete(h)
|
168
179
|
reset_hostname(h, reset_candidates: false)
|
169
180
|
|
170
|
-
|
181
|
+
unless @queries.empty?
|
182
|
+
resolve(connection)
|
183
|
+
return
|
184
|
+
end
|
171
185
|
|
172
186
|
@connections.delete(connection)
|
187
|
+
|
188
|
+
host = connection.peer.host
|
189
|
+
|
173
190
|
# This loop_time passed to the exception is bogus. Ideally we would pass the total
|
174
191
|
# resolve timeout, including from the previous retries.
|
175
|
-
ex = ResolveTimeoutError.new(
|
192
|
+
ex = ResolveTimeoutError.new(interval, "Timed out while resolving #{host}")
|
176
193
|
ex.set_backtrace(ex ? ex.backtrace : caller)
|
177
194
|
emit_resolve_error(connection, host, ex)
|
178
195
|
|
@@ -225,7 +242,7 @@ module HTTPX
|
|
225
242
|
parse(@read_buffer)
|
226
243
|
end
|
227
244
|
|
228
|
-
return if @state == :closed
|
245
|
+
return if @state == :closed || !@write_buffer.empty?
|
229
246
|
end
|
230
247
|
end
|
231
248
|
|
@@ -243,11 +260,15 @@ module HTTPX
|
|
243
260
|
|
244
261
|
return unless siz.positive?
|
245
262
|
|
263
|
+
schedule_retry if @write_buffer.empty?
|
264
|
+
|
246
265
|
return if @state == :closed
|
247
266
|
end
|
248
267
|
end
|
249
268
|
|
250
269
|
def parse(buffer)
|
270
|
+
@timer.cancel
|
271
|
+
|
251
272
|
code, result = Resolver.decode_dns_answer(buffer)
|
252
273
|
|
253
274
|
case code
|
@@ -258,8 +279,10 @@ module HTTPX
|
|
258
279
|
hostname, connection = @queries.first
|
259
280
|
reset_hostname(hostname, reset_candidates: false)
|
260
281
|
|
261
|
-
|
262
|
-
|
282
|
+
other_candidate, _ = @queries.find { |_, conn| conn == connection }
|
283
|
+
|
284
|
+
if other_candidate
|
285
|
+
resolve(connection, other_candidate)
|
263
286
|
else
|
264
287
|
@connections.delete(connection)
|
265
288
|
ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
|
@@ -321,8 +344,10 @@ module HTTPX
|
|
321
344
|
connection = @queries.delete(name)
|
322
345
|
end
|
323
346
|
|
324
|
-
|
325
|
-
|
347
|
+
alias_addresses, addresses = addresses.partition { |addr| addr.key?("alias") }
|
348
|
+
|
349
|
+
if addresses.empty? && !alias_addresses.empty? # CNAME
|
350
|
+
hostname_alias = alias_addresses.first["alias"]
|
326
351
|
# clean up intermediate queries
|
327
352
|
@timeouts.delete(name) unless connection.peer.host == name
|
328
353
|
|
@@ -350,7 +375,11 @@ module HTTPX
|
|
350
375
|
close_or_resolve
|
351
376
|
end
|
352
377
|
|
353
|
-
def resolve(connection =
|
378
|
+
def resolve(connection = nil, hostname = nil)
|
379
|
+
@connections.shift until @connections.empty? || @connections.first.state != :closed
|
380
|
+
|
381
|
+
connection ||= @connections.find { |c| !@queries.value?(c) }
|
382
|
+
|
354
383
|
raise Error, "no URI to resolve" unless connection
|
355
384
|
|
356
385
|
return unless @write_buffer.empty?
|
@@ -370,6 +399,9 @@ module HTTPX
|
|
370
399
|
else
|
371
400
|
@queries[hostname] = connection
|
372
401
|
end
|
402
|
+
|
403
|
+
@name = hostname
|
404
|
+
|
373
405
|
log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
|
374
406
|
begin
|
375
407
|
@write_buffer << encode_dns_query(hostname)
|
@@ -458,6 +490,7 @@ module HTTPX
|
|
458
490
|
# these errors may happen during TCP handshake
|
459
491
|
# treat them as resolve errors.
|
460
492
|
handle_error(e)
|
493
|
+
emit(:close, self)
|
461
494
|
end
|
462
495
|
|
463
496
|
def handle_error(error)
|
@@ -472,13 +505,15 @@ module HTTPX
|
|
472
505
|
@connections.delete(connection)
|
473
506
|
emit_resolve_error(connection, host, error)
|
474
507
|
end
|
508
|
+
|
509
|
+
while (connection = @connections.shift)
|
510
|
+
emit_resolve_error(connection, host, error)
|
511
|
+
end
|
475
512
|
end
|
476
|
-
close_or_resolve
|
477
513
|
end
|
478
514
|
|
479
515
|
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
480
516
|
@timeouts.delete(hostname)
|
481
|
-
@timeouts.delete(hostname)
|
482
517
|
|
483
518
|
return unless connection && reset_candidates
|
484
519
|
|
@@ -490,7 +525,10 @@ module HTTPX
|
|
490
525
|
end
|
491
526
|
|
492
527
|
def close_or_resolve
|
493
|
-
|
528
|
+
# drop already closed connections
|
529
|
+
@connections.shift until @connections.empty? || @connections.first.state != :closed
|
530
|
+
|
531
|
+
if (@connections - @queries.values).empty?
|
494
532
|
emit(:close, self)
|
495
533
|
else
|
496
534
|
resolve
|
@@ -74,14 +74,15 @@ module HTTPX
|
|
74
74
|
|
75
75
|
log do
|
76
76
|
"resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
|
77
|
-
"answer #{
|
77
|
+
"answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
|
78
78
|
end
|
79
79
|
|
80
|
-
if
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
80
|
+
if !early_resolve && # do not apply resolution delay for non-dns name resolution
|
81
|
+
@current_selector && # just in case...
|
82
|
+
family == Socket::AF_INET && # resolution delay only applies to IPv4
|
83
|
+
!connection.io && # connection already has addresses and initiated/ended handshake
|
84
|
+
connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
|
85
|
+
addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
|
85
86
|
log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
|
86
87
|
|
87
88
|
@current_selector.after(0.05) do
|
data/lib/httpx/selector.rb
CHANGED
@@ -19,6 +19,7 @@ module HTTPX
|
|
19
19
|
def initialize
|
20
20
|
@timers = Timers.new
|
21
21
|
@selectables = []
|
22
|
+
@is_timer_interval = false
|
22
23
|
end
|
23
24
|
|
24
25
|
def each(&blk)
|
@@ -43,7 +44,11 @@ module HTTPX
|
|
43
44
|
rescue StandardError => e
|
44
45
|
emit_error(e)
|
45
46
|
rescue Exception # rubocop:disable Lint/RescueException
|
46
|
-
each_connection
|
47
|
+
each_connection do |conn|
|
48
|
+
conn.force_reset
|
49
|
+
conn.disconnect
|
50
|
+
end
|
51
|
+
|
47
52
|
raise
|
48
53
|
end
|
49
54
|
|
@@ -125,24 +130,22 @@ module HTTPX
|
|
125
130
|
# first, we group IOs based on interest type. On call to #interests however,
|
126
131
|
# things might already happen, and new IOs might be registered, so we might
|
127
132
|
# have to start all over again. We do this until we group all selectables
|
128
|
-
|
129
|
-
|
130
|
-
interests = io.interests
|
133
|
+
@selectables.delete_if do |io|
|
134
|
+
interests = io.interests
|
131
135
|
|
132
|
-
|
133
|
-
|
136
|
+
(r ||= []) << io if READABLE.include?(interests)
|
137
|
+
(w ||= []) << io if WRITABLE.include?(interests)
|
134
138
|
|
135
|
-
|
136
|
-
|
139
|
+
io.state == :closed
|
140
|
+
end
|
137
141
|
|
138
|
-
|
142
|
+
# TODO: what to do if there are no selectables?
|
139
143
|
|
140
|
-
|
144
|
+
readers, writers = IO.select(r, w, nil, interval)
|
141
145
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
146
|
+
if readers.nil? && writers.nil? && interval
|
147
|
+
[*r, *w].each { |io| io.handle_socket_timeout(interval) }
|
148
|
+
return
|
146
149
|
end
|
147
150
|
|
148
151
|
if writers
|
@@ -174,7 +177,7 @@ module HTTPX
|
|
174
177
|
end
|
175
178
|
|
176
179
|
unless result || interval.nil?
|
177
|
-
io.handle_socket_timeout(interval)
|
180
|
+
io.handle_socket_timeout(interval) unless @is_timer_interval
|
178
181
|
return
|
179
182
|
end
|
180
183
|
# raise TimeoutError.new(interval, "timed out while waiting on select")
|
@@ -186,10 +189,21 @@ module HTTPX
|
|
186
189
|
end
|
187
190
|
|
188
191
|
def next_timeout
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
192
|
+
@is_timer_interval = false
|
193
|
+
|
194
|
+
timer_interval = @timers.wait_interval
|
195
|
+
|
196
|
+
connection_interval = @selectables.filter_map(&:timeout).min
|
197
|
+
|
198
|
+
return connection_interval unless timer_interval
|
199
|
+
|
200
|
+
if connection_interval.nil? || timer_interval <= connection_interval
|
201
|
+
@is_timer_interval = true
|
202
|
+
|
203
|
+
return timer_interval
|
204
|
+
end
|
205
|
+
|
206
|
+
connection_interval
|
193
207
|
end
|
194
208
|
|
195
209
|
def emit_error(e)
|
data/lib/httpx/session.rb
CHANGED
@@ -240,11 +240,9 @@ module HTTPX
|
|
240
240
|
end
|
241
241
|
return unless error && error.is_a?(Exception)
|
242
242
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
raise error if selector.empty?
|
247
|
-
end
|
243
|
+
raise error unless error.is_a?(Error)
|
244
|
+
|
245
|
+
request.emit(:response, ErrorResponse.new(request, error))
|
248
246
|
end
|
249
247
|
|
250
248
|
# returns a set of HTTPX::Request objects built from the given +args+ and +options+.
|
data/lib/httpx/timers.rb
CHANGED
@@ -26,7 +26,7 @@ module HTTPX
|
|
26
26
|
|
27
27
|
@next_interval_at = nil
|
28
28
|
|
29
|
-
interval
|
29
|
+
Timer.new(interval, callback)
|
30
30
|
end
|
31
31
|
|
32
32
|
def wait_interval
|
@@ -48,6 +48,17 @@ module HTTPX
|
|
48
48
|
@next_interval_at = nil if @intervals.empty?
|
49
49
|
end
|
50
50
|
|
51
|
+
class Timer
|
52
|
+
def initialize(interval, callback)
|
53
|
+
@interval = interval
|
54
|
+
@callback = callback
|
55
|
+
end
|
56
|
+
|
57
|
+
def cancel
|
58
|
+
@interval.delete(@callback)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
51
62
|
class Interval
|
52
63
|
include Comparable
|
53
64
|
|
@@ -63,6 +74,10 @@ module HTTPX
|
|
63
74
|
@on_empty = blk
|
64
75
|
end
|
65
76
|
|
77
|
+
def cancel
|
78
|
+
@on_empty.call
|
79
|
+
end
|
80
|
+
|
66
81
|
def <=>(other)
|
67
82
|
@interval <=> other.interval
|
68
83
|
end
|
data/lib/httpx/version.rb
CHANGED
data/sig/callbacks.rbs
CHANGED
@@ -4,8 +4,8 @@ module HTTPX
|
|
4
4
|
end
|
5
5
|
|
6
6
|
module Callbacks
|
7
|
-
def on: (Symbol) { (*untyped) -> void } ->
|
8
|
-
def once: (Symbol) { (*untyped) -> void } ->
|
7
|
+
def on: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
|
8
|
+
def once: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
|
9
9
|
def emit: (Symbol, *untyped) -> void
|
10
10
|
|
11
11
|
def callbacks_for?: (Symbol) -> bool
|
data/sig/connection.rbs
CHANGED
@@ -43,7 +43,6 @@ module HTTPX
|
|
43
43
|
@parser: Object & _Parser
|
44
44
|
@connected_at: Float
|
45
45
|
@response_received_at: Float
|
46
|
-
@intervals: Array[Timers::Interval]
|
47
46
|
@exhausted: bool
|
48
47
|
@cloned: bool
|
49
48
|
@coalesced_connection: instance?
|
@@ -111,6 +110,8 @@ module HTTPX
|
|
111
110
|
|
112
111
|
def handle_connect_error: (StandardError error) -> void
|
113
112
|
|
113
|
+
def disconnect: () -> void
|
114
|
+
|
114
115
|
private
|
115
116
|
|
116
117
|
def initialize: (http_uri uri, Options options) -> void
|
@@ -119,8 +120,6 @@ module HTTPX
|
|
119
120
|
|
120
121
|
def connect: () -> void
|
121
122
|
|
122
|
-
def disconnect: () -> void
|
123
|
-
|
124
123
|
def exhausted?: () -> boolish
|
125
124
|
|
126
125
|
def consume: () -> void
|
@@ -163,7 +162,7 @@ module HTTPX
|
|
163
162
|
|
164
163
|
def read_timeout_callback: (Request request, Numeric read_timeout, ?singleton(RequestTimeoutError) error_type) -> void
|
165
164
|
|
166
|
-
def set_request_timeout: (Request request, Numeric timeout, Symbol start_event, Symbol | Array[Symbol] finish_events) { () -> void } -> void
|
165
|
+
def set_request_timeout: (Symbol label, Request request, Numeric timeout, Symbol start_event, Symbol | Array[Symbol] finish_events) { () -> void } -> void
|
167
166
|
|
168
167
|
def self.parser_type: (String protocol) -> (singleton(HTTP1) | singleton(HTTP2))
|
169
168
|
end
|
data/sig/errors.rbs
CHANGED
data/sig/request.rbs
CHANGED
@@ -14,6 +14,7 @@ module HTTPX
|
|
14
14
|
attr_reader options: Options
|
15
15
|
attr_reader response: response?
|
16
16
|
attr_reader drain_error: StandardError?
|
17
|
+
attr_reader active_timeouts: Array[Symbol]
|
17
18
|
|
18
19
|
attr_accessor peer_address: ipaddr?
|
19
20
|
|
@@ -63,6 +64,8 @@ module HTTPX
|
|
63
64
|
|
64
65
|
def request_timeout: () -> Numeric?
|
65
66
|
|
67
|
+
def set_timeout_callback: (Symbol event) { (*untyped) -> void } -> void
|
68
|
+
|
66
69
|
private
|
67
70
|
|
68
71
|
def initialize_body: (Options options) -> Transcoder::_Encoder?
|
data/sig/resolver/native.rbs
CHANGED
@@ -21,6 +21,7 @@ module HTTPX
|
|
21
21
|
@write_buffer: Buffer
|
22
22
|
@large_packet: Buffer?
|
23
23
|
@io: UDP | TCP
|
24
|
+
@name: String?
|
24
25
|
|
25
26
|
attr_reader state: Symbol
|
26
27
|
|
@@ -42,7 +43,9 @@ module HTTPX
|
|
42
43
|
|
43
44
|
def consume: () -> void
|
44
45
|
|
45
|
-
def
|
46
|
+
def schedule_retry: () -> void
|
47
|
+
|
48
|
+
def do_retry: (String host, Connection connection, Numeric interval) -> void
|
46
49
|
|
47
50
|
def dread: (Integer) -> void
|
48
51
|
| () -> void
|
data/sig/selector.rbs
CHANGED
data/sig/timers.rbs
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
module HTTPX
|
2
2
|
class Timers
|
3
|
+
type callback = ^() -> void
|
4
|
+
|
3
5
|
@intervals: Array[Interval]
|
4
6
|
@next_interval_at: Float
|
5
7
|
|
6
|
-
def after: (Numeric interval_in_secs, ^() -> void) ->
|
7
|
-
| (Numeric interval_in_secs) { () -> void } ->
|
8
|
+
def after: (Numeric interval_in_secs, ^() -> void) -> Timer
|
9
|
+
| (Numeric interval_in_secs) { () -> void } -> Timer
|
8
10
|
|
9
11
|
def wait_interval: () -> Numeric?
|
10
12
|
|
@@ -15,8 +17,6 @@ module HTTPX
|
|
15
17
|
class Interval
|
16
18
|
include Comparable
|
17
19
|
|
18
|
-
type callback = ^() -> void
|
19
|
-
|
20
20
|
attr_reader interval: Numeric
|
21
21
|
|
22
22
|
@callbacks: Array[callback]
|
@@ -25,6 +25,8 @@ module HTTPX
|
|
25
25
|
|
26
26
|
def on_empty: () { () -> void } -> void
|
27
27
|
|
28
|
+
def cancel: () -> void
|
29
|
+
|
28
30
|
def to_f: () -> Float
|
29
31
|
|
30
32
|
def <<: (callback) -> void
|
@@ -41,5 +43,14 @@ module HTTPX
|
|
41
43
|
|
42
44
|
def initialize: (Numeric interval) -> void
|
43
45
|
end
|
46
|
+
|
47
|
+
class Timer
|
48
|
+
@interval: Interval
|
49
|
+
@callback: callback
|
50
|
+
|
51
|
+
def initialize: (Interval interval, callback callback) -> void
|
52
|
+
|
53
|
+
def cancel: () -> void
|
54
|
+
end
|
44
55
|
end
|
45
56
|
end
|
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.
|
4
|
+
version: 1.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -151,6 +151,7 @@ extra_rdoc_files:
|
|
151
151
|
- doc/release_notes/1_3_4.md
|
152
152
|
- doc/release_notes/1_4_0.md
|
153
153
|
- doc/release_notes/1_4_1.md
|
154
|
+
- doc/release_notes/1_4_2.md
|
154
155
|
files:
|
155
156
|
- LICENSE.txt
|
156
157
|
- README.md
|
@@ -273,6 +274,7 @@ files:
|
|
273
274
|
- doc/release_notes/1_3_4.md
|
274
275
|
- doc/release_notes/1_4_0.md
|
275
276
|
- doc/release_notes/1_4_1.md
|
277
|
+
- doc/release_notes/1_4_2.md
|
276
278
|
- lib/httpx.rb
|
277
279
|
- lib/httpx/adapters/datadog.rb
|
278
280
|
- lib/httpx/adapters/faraday.rb
|
@@ -493,7 +495,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
493
495
|
- !ruby/object:Gem::Version
|
494
496
|
version: '0'
|
495
497
|
requirements: []
|
496
|
-
rubygems_version: 3.5.
|
498
|
+
rubygems_version: 3.5.22
|
497
499
|
signing_key:
|
498
500
|
specification_version: 4
|
499
501
|
summary: HTTPX, to the future, and beyond
|