httpx 1.9.0 → 1.9.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 +4 -4
- data/doc/release_notes/1_8_1.md +1 -1
- data/doc/release_notes/1_8_2.md +4 -4
- data/doc/release_notes/1_8_3.md +3 -5
- data/doc/release_notes/1_8_4.md +1 -1
- data/doc/release_notes/1_9_0.md +7 -7
- data/doc/release_notes/1_9_1.md +32 -0
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/webmock.rb +14 -1
- data/lib/httpx/connection/http1.rb +24 -17
- data/lib/httpx/connection/http2.rb +108 -29
- data/lib/httpx/connection.rb +158 -162
- data/lib/httpx/io/tcp.rb +24 -7
- data/lib/httpx/plugins/aws_sigv4.rb +0 -1
- data/lib/httpx/plugins/cache.rb +1 -1
- data/lib/httpx/plugins/callbacks.rb +1 -0
- data/lib/httpx/plugins/content_digest.rb +3 -5
- data/lib/httpx/plugins/expect.rb +8 -1
- data/lib/httpx/plugins/fiber_concurrency.rb +56 -19
- data/lib/httpx/plugins/h2c.rb +12 -11
- data/lib/httpx/plugins/hedging.rb +2 -2
- data/lib/httpx/plugins/ntlm_auth.rb +4 -3
- data/lib/httpx/plugins/ntlm_v2_auth.rb +4 -3
- data/lib/httpx/plugins/oauth.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +23 -16
- data/lib/httpx/plugins/proxy/socks5.rb +4 -4
- data/lib/httpx/plugins/proxy.rb +8 -15
- data/lib/httpx/plugins/query.rb +1 -0
- data/lib/httpx/plugins/stream.rb +12 -10
- data/lib/httpx/plugins/stream_bidi.rb +4 -0
- data/lib/httpx/plugins/tracing.rb +62 -21
- data/lib/httpx/plugins/upgrade/h2.rb +2 -2
- data/lib/httpx/request.rb +1 -1
- data/lib/httpx/resolver/https.rb +3 -2
- data/lib/httpx/resolver/native.rb +19 -9
- data/lib/httpx/resolver/resolver.rb +2 -2
- data/lib/httpx/resolver/system.rb +4 -0
- data/lib/httpx/resolver.rb +5 -5
- data/lib/httpx/response.rb +5 -1
- data/lib/httpx/selector.rb +108 -7
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +1 -0
- data/sig/chainable.rbs +2 -1
- data/sig/connection/http1.rbs +4 -2
- data/sig/connection/http2.rbs +8 -2
- data/sig/connection.rbs +11 -6
- data/sig/io/tcp.rbs +5 -0
- data/sig/plugins/expect.rbs +6 -0
- data/sig/plugins/fiber_concurrency.rbs +10 -1
- data/sig/plugins/proxy/http.rbs +3 -1
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/tracing.rbs +10 -2
- data/sig/resolver/https.rbs +2 -0
- data/sig/resolver/native.rbs +3 -0
- data/sig/resolver/resolver.rbs +1 -1
- data/sig/resolver/system.rbs +2 -0
- data/sig/resolver.rbs +1 -1
- data/sig/selector.rbs +15 -1
- data/sig/session.rbs +1 -1
- metadata +3 -1
|
@@ -70,11 +70,14 @@ module HTTPX::Plugins
|
|
|
70
70
|
# the connection to connect.
|
|
71
71
|
attr_accessor :handshake_time
|
|
72
72
|
|
|
73
|
+
# time at which the request was first handed to a connection, or last moved back to waiting for one.
|
|
74
|
+
attr_accessor :enqueue_time
|
|
75
|
+
|
|
73
76
|
# intercepts request initialization to inject the tracing logic.
|
|
74
77
|
def initialize(*)
|
|
75
78
|
super
|
|
76
79
|
|
|
77
|
-
@init_time = @handshake_time = nil
|
|
80
|
+
@init_time = @handshake_time = @enqueue_time = nil
|
|
78
81
|
|
|
79
82
|
tracer = @options.tracer
|
|
80
83
|
|
|
@@ -85,12 +88,14 @@ module HTTPX::Plugins
|
|
|
85
88
|
|
|
86
89
|
# request is reset when it's retried.
|
|
87
90
|
@init_time = @handshake_time = nil
|
|
91
|
+
|
|
92
|
+
# going back to idle means waiting to be sent again.
|
|
93
|
+
@enqueue_time = ::Time.now.utc
|
|
88
94
|
end
|
|
89
95
|
on(:headers) do
|
|
90
96
|
# the usual request init time (when not including the connection handshake)
|
|
91
97
|
# should be the time the request is buffered the first time.
|
|
92
|
-
@init_time =
|
|
93
|
-
@init_time -= @handshake_time if @handshake_time
|
|
98
|
+
@init_time = calculate_init_time
|
|
94
99
|
|
|
95
100
|
tracer.start(self)
|
|
96
101
|
end
|
|
@@ -102,14 +107,27 @@ module HTTPX::Plugins
|
|
|
102
107
|
# Example is the :ssrf_filter plugin, which raises an error on
|
|
103
108
|
# initialize if the host is an IP which matches against the known set.
|
|
104
109
|
# in such cases, we'll just set here right here.
|
|
105
|
-
|
|
106
|
-
@init_time = ::Time.now.utc
|
|
107
|
-
|
|
108
|
-
@init_time -= @handshake_time if @handshake_time
|
|
109
|
-
end
|
|
110
|
+
@init_time ||= calculate_init_time
|
|
110
111
|
|
|
111
112
|
super
|
|
112
113
|
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def calculate_init_time
|
|
118
|
+
init_time = ::Time.now.utc
|
|
119
|
+
|
|
120
|
+
return init_time unless (handshake_time = @handshake_time)
|
|
121
|
+
|
|
122
|
+
init_time -= handshake_time
|
|
123
|
+
|
|
124
|
+
enqueue_time = @enqueue_time
|
|
125
|
+
|
|
126
|
+
# handshake time is measured for the connection, and may happen before the request was sent to it.
|
|
127
|
+
return init_time unless enqueue_time && init_time < enqueue_time
|
|
128
|
+
|
|
129
|
+
enqueue_time
|
|
130
|
+
end
|
|
113
131
|
end
|
|
114
132
|
|
|
115
133
|
# Connection mixin
|
|
@@ -120,7 +138,15 @@ module HTTPX::Plugins
|
|
|
120
138
|
@init_time = nil
|
|
121
139
|
end
|
|
122
140
|
|
|
123
|
-
def
|
|
141
|
+
def send(request)
|
|
142
|
+
# requests may be rerouted to other connections (i.e. when coalescing), so only the first
|
|
143
|
+
# connection the request is handed to marks it.
|
|
144
|
+
request.enqueue_time ||= ::Time.now.utc
|
|
145
|
+
|
|
146
|
+
super
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def send_request_to_parser(request, *)
|
|
124
150
|
if connecting? && @init_time
|
|
125
151
|
# at this point, the connection has connected, but hasn't yet transitioned to :open,
|
|
126
152
|
# so it's the time the requests which caused this connection to connect, are finally being
|
|
@@ -144,8 +170,26 @@ module HTTPX::Plugins
|
|
|
144
170
|
@init_time = nil
|
|
145
171
|
end
|
|
146
172
|
|
|
173
|
+
def disconnect
|
|
174
|
+
return if @exhausted
|
|
175
|
+
|
|
176
|
+
return unless @current_session && @current_selector
|
|
177
|
+
|
|
178
|
+
# reset init time of connections abandoned while connecting.
|
|
179
|
+
@init_time = nil if connecting?
|
|
180
|
+
|
|
181
|
+
super
|
|
182
|
+
end
|
|
183
|
+
|
|
147
184
|
private
|
|
148
185
|
|
|
186
|
+
def handle_transition(*)
|
|
187
|
+
super
|
|
188
|
+
|
|
189
|
+
# no longer relevant after connection established.
|
|
190
|
+
@init_time = nil unless connecting?
|
|
191
|
+
end
|
|
192
|
+
|
|
149
193
|
def handle_error(er, request = nil)
|
|
150
194
|
if connecting? && @init_time
|
|
151
195
|
# bookkeep handshake time back into pending requests, which were never initialized with it.
|
|
@@ -176,24 +220,21 @@ module HTTPX::Plugins
|
|
|
176
220
|
def initialize(*)
|
|
177
221
|
super
|
|
178
222
|
|
|
179
|
-
@handshake_time = nil
|
|
180
223
|
@handshake_init_time = ::Time.now.utc
|
|
181
224
|
end
|
|
182
225
|
|
|
183
|
-
def send(request, head = false)
|
|
184
|
-
if head
|
|
185
|
-
# only true for pending requests waiting on the handshake, so handshake time accrues.
|
|
186
|
-
request.handshake_time ||= 0.0
|
|
187
|
-
request.handshake_time += @handshake_time
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
super
|
|
191
|
-
end
|
|
192
|
-
|
|
193
226
|
private
|
|
194
227
|
|
|
195
228
|
def on_settings(*)
|
|
196
|
-
|
|
229
|
+
unless @handshake_completed
|
|
230
|
+
# pending requests must wait for the handshake completion
|
|
231
|
+
handshake_time = ::Time.now.utc - @handshake_init_time
|
|
232
|
+
|
|
233
|
+
@pending.each do |request|
|
|
234
|
+
request.handshake_time ||= 0.0
|
|
235
|
+
request.handshake_time += handshake_time
|
|
236
|
+
end
|
|
237
|
+
end
|
|
197
238
|
|
|
198
239
|
super
|
|
199
240
|
end
|
|
@@ -33,9 +33,9 @@ module HTTPX
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def upgrade_to_h2
|
|
36
|
-
|
|
36
|
+
@parser.reset_requests
|
|
37
37
|
|
|
38
|
-
@parser = @options.http2_class.new(@write_buffer, @options)
|
|
38
|
+
@parser = @options.http2_class.new(@write_buffer, @pending, @options)
|
|
39
39
|
set_parser_callbacks(@parser)
|
|
40
40
|
@upgrade_protocol = "h2"
|
|
41
41
|
|
data/lib/httpx/request.rb
CHANGED
data/lib/httpx/resolver/https.rb
CHANGED
|
@@ -32,7 +32,8 @@ module HTTPX
|
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
34
|
def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
|
|
35
|
-
:closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
|
|
35
|
+
:closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout,
|
|
36
|
+
:closed_for_select?
|
|
36
37
|
|
|
37
38
|
def initialize(_, options)
|
|
38
39
|
super
|
|
@@ -49,7 +50,7 @@ module HTTPX
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def state
|
|
52
|
-
@resolver_connection
|
|
53
|
+
@resolver_connection&.state || :idle
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def <<(connection)
|
|
@@ -38,12 +38,11 @@ module HTTPX
|
|
|
38
38
|
@search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
|
|
39
39
|
@_timeouts = Array(@resolver_options[:timeouts])
|
|
40
40
|
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
|
41
|
-
@name = nil
|
|
41
|
+
@name = @message_id = @timer = nil
|
|
42
42
|
@queries = {}
|
|
43
43
|
@read_buffer = "".b
|
|
44
44
|
@write_buffer = Buffer.new(@resolver_options[:packet_size])
|
|
45
45
|
@state = :idle
|
|
46
|
-
@timer = nil
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def close
|
|
@@ -52,7 +51,7 @@ module HTTPX
|
|
|
52
51
|
|
|
53
52
|
def force_close(*)
|
|
54
53
|
@timer.cancel if @timer
|
|
55
|
-
@timer = @name = nil
|
|
54
|
+
@timer = @name = @message_id = nil
|
|
56
55
|
@queries.clear
|
|
57
56
|
@timeouts.clear
|
|
58
57
|
close
|
|
@@ -87,6 +86,10 @@ module HTTPX
|
|
|
87
86
|
end
|
|
88
87
|
end
|
|
89
88
|
|
|
89
|
+
def closed_for_select?
|
|
90
|
+
@state == :closed
|
|
91
|
+
end
|
|
92
|
+
|
|
90
93
|
def interests
|
|
91
94
|
case @state
|
|
92
95
|
when :idle
|
|
@@ -170,6 +173,7 @@ module HTTPX
|
|
|
170
173
|
if nameserver && @ns_index < nameserver.size
|
|
171
174
|
log { "resolver #{FAMILY_TYPES[@record_type]}: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
|
|
172
175
|
transition(:idle)
|
|
176
|
+
transition(:open)
|
|
173
177
|
@timeouts.clear
|
|
174
178
|
retry
|
|
175
179
|
else
|
|
@@ -195,7 +199,7 @@ module HTTPX
|
|
|
195
199
|
@timer = @current_selector.after(timeout) do
|
|
196
200
|
next unless @connections.include?(connection)
|
|
197
201
|
|
|
198
|
-
@timer = @name = nil
|
|
202
|
+
@timer = @name = @message_id = nil
|
|
199
203
|
|
|
200
204
|
do_retry(h, connection, timeout)
|
|
201
205
|
end
|
|
@@ -310,7 +314,13 @@ module HTTPX
|
|
|
310
314
|
end
|
|
311
315
|
|
|
312
316
|
def parse(buffer)
|
|
313
|
-
code, result = Resolver.decode_dns_answer(buffer)
|
|
317
|
+
code, result, message_id = Resolver.decode_dns_answer(buffer)
|
|
318
|
+
|
|
319
|
+
# discard late message answers for no longer inflight queries.
|
|
320
|
+
if message_id && message_id != @message_id
|
|
321
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: discarding answer for a query no longer in flight" }
|
|
322
|
+
return
|
|
323
|
+
end
|
|
314
324
|
|
|
315
325
|
case code
|
|
316
326
|
when :ok
|
|
@@ -392,7 +402,7 @@ module HTTPX
|
|
|
392
402
|
orig_name = name
|
|
393
403
|
# absolute name
|
|
394
404
|
name_labels = Resolv::DNS::Name.create(name).to_a
|
|
395
|
-
name = @queries.each_key.
|
|
405
|
+
name = @queries.each_key.find { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
|
|
396
406
|
|
|
397
407
|
# probably a retried query for which there's an answer
|
|
398
408
|
unless name
|
|
@@ -462,11 +472,10 @@ module HTTPX
|
|
|
462
472
|
@queries[hostname] = connection
|
|
463
473
|
end
|
|
464
474
|
|
|
465
|
-
@name = hostname
|
|
466
|
-
|
|
467
475
|
log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
|
|
468
476
|
begin
|
|
469
477
|
@write_buffer << encode_dns_query(hostname)
|
|
478
|
+
@name = hostname
|
|
470
479
|
rescue Resolv::DNS::EncodeError => e
|
|
471
480
|
reset_hostname(hostname, connection: connection)
|
|
472
481
|
@connections.delete(connection)
|
|
@@ -479,6 +488,7 @@ module HTTPX
|
|
|
479
488
|
message_id = Resolver.generate_id
|
|
480
489
|
msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
|
|
481
490
|
msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
|
|
491
|
+
@message_id = message_id
|
|
482
492
|
msg
|
|
483
493
|
end
|
|
484
494
|
|
|
@@ -574,7 +584,7 @@ module HTTPX
|
|
|
574
584
|
def reset_query
|
|
575
585
|
@timer.cancel
|
|
576
586
|
|
|
577
|
-
@timer = @name = nil
|
|
587
|
+
@timer = @name = @message_id = nil
|
|
578
588
|
end
|
|
579
589
|
|
|
580
590
|
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
|
@@ -29,9 +29,9 @@ module HTTPX
|
|
|
29
29
|
|
|
30
30
|
attr_reader :family, :options
|
|
31
31
|
|
|
32
|
-
attr_writer :
|
|
32
|
+
attr_writer :current_session
|
|
33
33
|
|
|
34
|
-
attr_accessor :multi
|
|
34
|
+
attr_accessor :current_selector, :multi
|
|
35
35
|
|
|
36
36
|
def initialize(family, options)
|
|
37
37
|
@family = family
|
data/lib/httpx/resolver.rb
CHANGED
|
@@ -51,9 +51,9 @@ module HTTPX
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# no domain was found
|
|
54
|
-
return :no_domain_found if message.rcode == Resolv::DNS::RCode::NXDomain
|
|
54
|
+
return :no_domain_found, nil, message.id if message.rcode == Resolv::DNS::RCode::NXDomain
|
|
55
55
|
|
|
56
|
-
return :message_truncated if message.tc == 1
|
|
56
|
+
return :message_truncated, nil, message.id if message.tc == 1
|
|
57
57
|
|
|
58
58
|
if message.rcode != Resolv::DNS::RCode::NoError
|
|
59
59
|
case message.rcode
|
|
@@ -62,9 +62,9 @@ module HTTPX
|
|
|
62
62
|
# but the DNS resolver state.
|
|
63
63
|
Resolv::DNS::RCode::NotImp,
|
|
64
64
|
Resolv::DNS::RCode::Refused
|
|
65
|
-
return :retriable_error, message.rcode
|
|
65
|
+
return :retriable_error, message.rcode, message.id
|
|
66
66
|
else
|
|
67
|
-
return :dns_error, message.rcode
|
|
67
|
+
return :dns_error, message.rcode, message.id
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -89,7 +89,7 @@ module HTTPX
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
[:ok, addresses]
|
|
92
|
+
[:ok, addresses, message.id]
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
private
|
data/lib/httpx/response.rb
CHANGED
|
@@ -259,7 +259,11 @@ module HTTPX
|
|
|
259
259
|
if c
|
|
260
260
|
c.strip!
|
|
261
261
|
c.delete_prefix!("charset=")
|
|
262
|
-
c
|
|
262
|
+
c.gsub!(/\A"(.*?)(?<!\\)"\z/) do
|
|
263
|
+
c2 = ::Regexp.last_match(1)
|
|
264
|
+
c2.gsub!(/\\(.)/, '\1')
|
|
265
|
+
c2
|
|
266
|
+
end
|
|
263
267
|
@charset = c
|
|
264
268
|
end
|
|
265
269
|
end
|
data/lib/httpx/selector.rb
CHANGED
|
@@ -21,11 +21,17 @@ module HTTPX
|
|
|
21
21
|
class Selector
|
|
22
22
|
extend Forwardable
|
|
23
23
|
|
|
24
|
+
HAS_FIBER_SCHEDULER = Fiber.respond_to?(:scheduler)
|
|
25
|
+
|
|
24
26
|
READABLE = %i[rw r].freeze
|
|
25
27
|
WRITABLE = %i[rw w].freeze
|
|
26
28
|
|
|
29
|
+
# max number of consecutive ticks allowed to neither wait on IO nor call a ready selectable.
|
|
30
|
+
MAX_BUSY_ITERATIONS = 1000
|
|
31
|
+
|
|
27
32
|
private_constant :READABLE
|
|
28
33
|
private_constant :WRITABLE
|
|
34
|
+
private_constant :MAX_BUSY_ITERATIONS
|
|
29
35
|
|
|
30
36
|
def_delegator :@timers, :after
|
|
31
37
|
|
|
@@ -35,6 +41,8 @@ module HTTPX
|
|
|
35
41
|
@timers = Timers.new
|
|
36
42
|
@selectables = []
|
|
37
43
|
@is_timer_interval = false
|
|
44
|
+
@busy_iterations = 0
|
|
45
|
+
@last_select_registered = nil
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def empty?
|
|
@@ -51,11 +59,16 @@ module HTTPX
|
|
|
51
59
|
timeout = next_timeout
|
|
52
60
|
if timeout && timeout.negative?
|
|
53
61
|
@timers.fire
|
|
62
|
+
busy_loop_check(timeout)
|
|
54
63
|
throw(:jump_tick)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
begin
|
|
67
|
+
selected = false
|
|
68
|
+
|
|
58
69
|
select(timeout) do |c|
|
|
70
|
+
selected = true
|
|
71
|
+
|
|
59
72
|
c.log(level: 2) { "[#{c.state}] selected from selector##{object_id}#{" after #{timeout} secs" unless timeout.nil?}..." }
|
|
60
73
|
|
|
61
74
|
c.call
|
|
@@ -64,6 +77,19 @@ module HTTPX
|
|
|
64
77
|
@timers.fire
|
|
65
78
|
rescue TimeoutError => e
|
|
66
79
|
@timers.fire(e)
|
|
80
|
+
|
|
81
|
+
@busy_iterations = 0
|
|
82
|
+
else
|
|
83
|
+
# a tick is healthy if it either called a ready selectable, or actually blocked waiting for one.
|
|
84
|
+
# without a timeout, #select only blocks when something registered interests; with one,
|
|
85
|
+
# it always sleeps or selects, but a zero timeout just polls.
|
|
86
|
+
waited = timeout.nil? ? @last_select_registered.to_i.positive? : timeout.positive?
|
|
87
|
+
|
|
88
|
+
if selected || waited
|
|
89
|
+
@busy_iterations = 0
|
|
90
|
+
else
|
|
91
|
+
busy_loop_check(timeout)
|
|
92
|
+
end
|
|
67
93
|
end
|
|
68
94
|
end
|
|
69
95
|
end
|
|
@@ -74,7 +100,9 @@ module HTTPX
|
|
|
74
100
|
|
|
75
101
|
selectables.delete_if do |sel|
|
|
76
102
|
sel.terminate
|
|
77
|
-
|
|
103
|
+
# TODO: this check is racy: connections check in to the pool on termination, and may check out and
|
|
104
|
+
# change state immediately after. find a more idempotent way of doing this.
|
|
105
|
+
sel.current_selector != self
|
|
78
106
|
end
|
|
79
107
|
|
|
80
108
|
until selectables.empty?
|
|
@@ -133,6 +161,8 @@ module HTTPX
|
|
|
133
161
|
private
|
|
134
162
|
|
|
135
163
|
def select(interval, &block)
|
|
164
|
+
@last_select_registered = nil
|
|
165
|
+
|
|
136
166
|
# do not cause an infinite loop here.
|
|
137
167
|
#
|
|
138
168
|
# this may happen if timeout calculation actually triggered an error which causes
|
|
@@ -153,10 +183,12 @@ module HTTPX
|
|
|
153
183
|
# @type var w: (selectable | Array[selectable])?
|
|
154
184
|
r, w = nil
|
|
155
185
|
|
|
186
|
+
registered = 0
|
|
187
|
+
|
|
156
188
|
@selectables.delete_if do |io|
|
|
157
189
|
interests = io.interests
|
|
158
190
|
|
|
159
|
-
is_closed = io.
|
|
191
|
+
is_closed = io.closed_for_select?
|
|
160
192
|
|
|
161
193
|
if is_closed
|
|
162
194
|
# the process by which io was closed may have already triggered the on_close callback,
|
|
@@ -166,6 +198,8 @@ module HTTPX
|
|
|
166
198
|
end
|
|
167
199
|
|
|
168
200
|
if interests
|
|
201
|
+
registered += 1
|
|
202
|
+
|
|
169
203
|
io.log(level: 2) do
|
|
170
204
|
"[#{io.state}] registering in selector##{object_id} for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}"
|
|
171
205
|
end
|
|
@@ -182,6 +216,8 @@ module HTTPX
|
|
|
182
216
|
is_closed
|
|
183
217
|
end
|
|
184
218
|
|
|
219
|
+
@last_select_registered = registered
|
|
220
|
+
|
|
185
221
|
case r
|
|
186
222
|
when Array
|
|
187
223
|
w = Array(w) unless w.nil?
|
|
@@ -192,7 +228,14 @@ module HTTPX
|
|
|
192
228
|
when Array
|
|
193
229
|
select_many(r, w, interval, &block)
|
|
194
230
|
when nil
|
|
195
|
-
|
|
231
|
+
unless interval && @selectables.any?
|
|
232
|
+
# @fiber-switch-guard
|
|
233
|
+
# may mean that another fiber is moving a selectable forward (i.e. closing its
|
|
234
|
+
# socket), so let the scheduler resume it.
|
|
235
|
+
sleep(0) if HAS_FIBER_SCHEDULER && Fiber.scheduler
|
|
236
|
+
|
|
237
|
+
return
|
|
238
|
+
end
|
|
196
239
|
|
|
197
240
|
# no selectables
|
|
198
241
|
# TODO: replace with sleep?
|
|
@@ -221,18 +264,19 @@ module HTTPX
|
|
|
221
264
|
begin
|
|
222
265
|
readers, writers = ::IO.select(r, w, nil, interval)
|
|
223
266
|
rescue IOError => e
|
|
224
|
-
(
|
|
225
|
-
# TODO: is there a way to cheaply find the IO associated with the error?
|
|
267
|
+
selectables_with_closed_ios(r, w).each do |sel|
|
|
226
268
|
sel.on_io_error(e)
|
|
227
269
|
end
|
|
270
|
+
|
|
271
|
+
return
|
|
228
272
|
rescue StandardError => e
|
|
229
|
-
(
|
|
273
|
+
selectables_with_closed_ios(r, w).each do |sel|
|
|
230
274
|
sel.on_error(e)
|
|
231
275
|
end
|
|
232
276
|
|
|
233
277
|
return
|
|
234
278
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
235
|
-
|
|
279
|
+
Set[*Array(r), *Array(w)].each do |sel|
|
|
236
280
|
sel.force_close(true)
|
|
237
281
|
end
|
|
238
282
|
|
|
@@ -258,6 +302,16 @@ module HTTPX
|
|
|
258
302
|
end
|
|
259
303
|
end
|
|
260
304
|
|
|
305
|
+
# returns the selectables whose IOs were closed.
|
|
306
|
+
def selectables_with_closed_ios(r, w)
|
|
307
|
+
selectables = Set[*Array(r), *Array(w)]
|
|
308
|
+
|
|
309
|
+
closed = selectables.select { |sel| sel.to_io.closed? }
|
|
310
|
+
|
|
311
|
+
# When none of them was closed, the error can't be attributed to any of them, so all are returned.
|
|
312
|
+
closed.empty? ? selectables : closed
|
|
313
|
+
end
|
|
314
|
+
|
|
261
315
|
def select_one(io, interests, interval)
|
|
262
316
|
begin
|
|
263
317
|
result =
|
|
@@ -306,6 +360,53 @@ module HTTPX
|
|
|
306
360
|
connection_interval
|
|
307
361
|
end
|
|
308
362
|
|
|
363
|
+
def busy_loop_check(timeout)
|
|
364
|
+
@busy_iterations += 1
|
|
365
|
+
|
|
366
|
+
return if @busy_iterations < MAX_BUSY_ITERATIONS
|
|
367
|
+
|
|
368
|
+
# this selector is reused by subsequent requests in the same thread/fiber.
|
|
369
|
+
@busy_iterations = 0
|
|
370
|
+
|
|
371
|
+
raise Error, busy_loop_error_message(timeout)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def busy_loop_error_message(timeout)
|
|
375
|
+
selectables_state = @selectables.map do |sel|
|
|
376
|
+
state = +"#{sel.class}:#{sel.object_id}[state:#{sel.state},inflight?:#{sel.inflight?}"
|
|
377
|
+
|
|
378
|
+
case sel
|
|
379
|
+
when Connection
|
|
380
|
+
state << ",origin:#{sel.origin},pending:#{sel.pending.size}," \
|
|
381
|
+
"io-state:#{sel.io&.state.inspect},io-interests:#{sel.io&.interests.inspect}," \
|
|
382
|
+
"in-session:#{!sel.current_session.nil?}"
|
|
383
|
+
when Resolver::Resolver
|
|
384
|
+
num_connections = 0
|
|
385
|
+
sel.each_connection { num_connections += 1 }
|
|
386
|
+
state << ",connections:#{num_connections}"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
state << "]"
|
|
390
|
+
end.join(",")
|
|
391
|
+
|
|
392
|
+
timers_state = @timers.instance_variable_get(:@intervals).map do |interval|
|
|
393
|
+
"#{interval.interval}s(callbacks:#{interval.instance_variable_get(:@callbacks).size})"
|
|
394
|
+
end.join(",")
|
|
395
|
+
|
|
396
|
+
"selector corrupted, aborted after looping #{MAX_BUSY_ITERATIONS} times " \
|
|
397
|
+
"without waiting on IO nor calling a ready selectable, " \
|
|
398
|
+
"please report this https://gitlab.com/os85/httpx/-/work_items " \
|
|
399
|
+
"along with debug logs\n(" \
|
|
400
|
+
"next-timeout:#{timeout.inspect}," \
|
|
401
|
+
"is-timer-interval:#{@is_timer_interval}," \
|
|
402
|
+
"timers:[#{timers_state}]," \
|
|
403
|
+
"next-interval-at:#{@timers.instance_variable_get(:@next_interval_at).inspect}," \
|
|
404
|
+
"last-select-registered:#{@last_select_registered.inspect}," \
|
|
405
|
+
"num-selectables:#{@selectables.size}," \
|
|
406
|
+
"selectables:[#{selectables_state}]" \
|
|
407
|
+
")"
|
|
408
|
+
end
|
|
409
|
+
|
|
309
410
|
if RUBY_ENGINE == "jruby"
|
|
310
411
|
def rw_wait(io, interval)
|
|
311
412
|
io.to_io.wait(interval, :read_write)
|
|
@@ -56,7 +56,7 @@ module HTTPX::Transcoder
|
|
|
56
56
|
def json_load(...); MultiJson.load(...); end
|
|
57
57
|
def json_dump(...); MultiJson.dump(...); end
|
|
58
58
|
elsif defined?(Oj)
|
|
59
|
-
def json_load(response,
|
|
59
|
+
def json_load(response, options = {}); Oj.load(response.to_s, { mode: :strict }.merge(options)); end
|
|
60
60
|
def json_dump(obj, options = {}); Oj.dump(obj, { mode: :compat }.merge(options)); end
|
|
61
61
|
elsif defined?(Yajl)
|
|
62
62
|
def json_load(response, ...); Yajl::Parser.new(...).parse(response.to_s); end
|
data/lib/httpx/version.rb
CHANGED
data/lib/httpx.rb
CHANGED
data/sig/chainable.rbs
CHANGED
|
@@ -2,7 +2,8 @@ module HTTPX
|
|
|
2
2
|
module Chainable
|
|
3
3
|
def request: (Request, **untyped) -> response
|
|
4
4
|
| (*Request, **untyped) -> Array[response]
|
|
5
|
-
| (verb, uri
|
|
5
|
+
| (verb, uri, **untyped) -> response
|
|
6
|
+
| (verb, [uri], **untyped) -> response
|
|
6
7
|
| (Array[[verb, uri] | [verb, uri, request_params]], **untyped) -> Array[response]
|
|
7
8
|
| (verb, _Each[uri | [uri, request_params]], **untyped) -> Array[response]
|
|
8
9
|
|
data/sig/connection/http1.rbs
CHANGED
|
@@ -36,7 +36,7 @@ module HTTPX
|
|
|
36
36
|
|
|
37
37
|
def <<: (string) -> void
|
|
38
38
|
|
|
39
|
-
def send: (Request request, ?bool head) ->
|
|
39
|
+
def send: (Request request, ?bool head) -> bool
|
|
40
40
|
|
|
41
41
|
def consume: () -> void
|
|
42
42
|
|
|
@@ -60,9 +60,11 @@ module HTTPX
|
|
|
60
60
|
|
|
61
61
|
def timeout: () -> interval?
|
|
62
62
|
|
|
63
|
+
def no_more_requests?: () -> bool
|
|
64
|
+
|
|
63
65
|
private
|
|
64
66
|
|
|
65
|
-
def initialize: (Buffer buffer, Options options) -> untyped
|
|
67
|
+
def initialize: (Buffer buffer, Array[Request] pending, Options options) -> untyped
|
|
66
68
|
|
|
67
69
|
def manage_connection: (Request request, Response response) -> void
|
|
68
70
|
|
data/sig/connection/http2.rbs
CHANGED
|
@@ -20,6 +20,7 @@ module HTTPX
|
|
|
20
20
|
@buffer: Buffer
|
|
21
21
|
@handshake_completed: bool
|
|
22
22
|
@wait_for_handshake: bool
|
|
23
|
+
@last_error: GoawayError?
|
|
23
24
|
|
|
24
25
|
def interests: () -> io_interests?
|
|
25
26
|
|
|
@@ -47,11 +48,15 @@ module HTTPX
|
|
|
47
48
|
|
|
48
49
|
def reset_requests: () -> void
|
|
49
50
|
|
|
51
|
+
def no_more_requests?: () -> bool
|
|
52
|
+
|
|
50
53
|
private
|
|
51
54
|
|
|
52
|
-
def initialize: (Buffer buffer, Options options) -> untyped
|
|
55
|
+
def initialize: (Buffer buffer, Array[Request] pending, Options options) -> untyped
|
|
56
|
+
|
|
57
|
+
def waiting_for_peer?: () -> bool
|
|
53
58
|
|
|
54
|
-
def
|
|
59
|
+
def can_buffer_pending_requests?: () -> bool
|
|
55
60
|
|
|
56
61
|
def send_pending: () -> void
|
|
57
62
|
|
|
@@ -119,6 +124,7 @@ module HTTPX
|
|
|
119
124
|
UNRECOVERABLE_ERRORS: Array[Symbol]
|
|
120
125
|
|
|
121
126
|
@code: Symbol
|
|
127
|
+
@last_stream_id: Integer
|
|
122
128
|
|
|
123
129
|
def initialize: (Symbol code, Integer last_stream_id) -> void
|
|
124
130
|
|