httpx 1.6.3 → 1.7.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/0_11_0.md +3 -3
- data/doc/release_notes/1_6_3.md +2 -2
- data/doc/release_notes/1_7_0.md +149 -0
- data/doc/release_notes/1_7_1.md +21 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/webmock.rb +18 -9
- data/lib/httpx/altsvc.rb +4 -2
- data/lib/httpx/connection/http1.rb +9 -9
- data/lib/httpx/connection/http2.rb +2 -0
- data/lib/httpx/connection.rb +7 -9
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/tcp.rb +1 -1
- data/lib/httpx/loggable.rb +2 -0
- data/lib/httpx/options.rb +118 -22
- data/lib/httpx/parser/http1.rb +1 -0
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +113 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
- data/lib/httpx/plugins/cookies/cookie.rb +1 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -5
- data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +156 -57
- data/lib/httpx/plugins/persistent.rb +3 -5
- data/lib/httpx/plugins/proxy/http.rb +0 -4
- data/lib/httpx/plugins/proxy.rb +3 -1
- data/lib/httpx/plugins/query.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +20 -15
- data/lib/httpx/plugins/response_cache.rb +3 -7
- data/lib/httpx/plugins/retries.rb +60 -24
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +60 -9
- data/lib/httpx/plugins/stream_bidi.rb +84 -16
- data/lib/httpx/pool.rb +12 -3
- data/lib/httpx/request/body.rb +1 -1
- data/lib/httpx/request.rb +10 -1
- data/lib/httpx/resolver/cache/base.rb +136 -0
- data/lib/httpx/resolver/cache/memory.rb +42 -0
- data/lib/httpx/resolver/cache.rb +18 -0
- data/lib/httpx/resolver/https.rb +74 -20
- data/lib/httpx/resolver/multi.rb +10 -2
- data/lib/httpx/resolver/native.rb +32 -6
- data/lib/httpx/resolver/resolver.rb +3 -3
- data/lib/httpx/resolver.rb +36 -114
- data/lib/httpx/response/body.rb +5 -3
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +14 -3
- data/lib/httpx/session.rb +6 -6
- data/lib/httpx/timers.rb +6 -12
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/gzip.rb +7 -2
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +5 -5
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +17 -9
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/utils.rb +13 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +9 -3
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +1 -3
- data/sig/loggable.rbs +1 -1
- data/sig/options.rbs +12 -4
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +37 -4
- data/sig/plugins/basic_auth.rbs +3 -3
- data/sig/plugins/digest_auth.rbs +2 -4
- data/sig/plugins/fiber_concurrency.rbs +6 -0
- data/sig/plugins/ntlm_auth.rbs +2 -2
- data/sig/plugins/oauth.rbs +44 -15
- data/sig/plugins/rate_limiter.rbs +4 -2
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +12 -4
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/resolver/cache/base.rbs +28 -0
- data/sig/resolver/cache/memory.rbs +13 -0
- data/sig/resolver/cache.rbs +16 -0
- data/sig/resolver/https.rbs +24 -0
- data/sig/resolver/multi.rbs +8 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver.rbs +5 -20
- data/sig/response.rbs +3 -0
- data/sig/session.rbs +3 -5
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- data/sig/utils.rbs +2 -0
- metadata +11 -1
data/lib/httpx/resolver/https.rb
CHANGED
|
@@ -12,6 +12,7 @@ module HTTPX
|
|
|
12
12
|
#
|
|
13
13
|
class Resolver::HTTPS < Resolver::Resolver
|
|
14
14
|
extend Forwardable
|
|
15
|
+
|
|
15
16
|
using URIExtensions
|
|
16
17
|
|
|
17
18
|
module DNSExtensions
|
|
@@ -30,7 +31,7 @@ module HTTPX
|
|
|
30
31
|
use_get: false,
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
33
|
-
def_delegators :@resolver_connection, :
|
|
34
|
+
def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
|
|
34
35
|
:closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
|
|
35
36
|
|
|
36
37
|
def initialize(_, options)
|
|
@@ -38,17 +39,23 @@ module HTTPX
|
|
|
38
39
|
@resolver_options = DEFAULTS.merge(@options.resolver_options)
|
|
39
40
|
@queries = {}
|
|
40
41
|
@requests = {}
|
|
42
|
+
@_timeouts = Array(@resolver_options[:timeouts])
|
|
43
|
+
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
|
41
44
|
@uri = URI(@resolver_options[:uri])
|
|
42
|
-
@uri_addresses = nil
|
|
45
|
+
@name = @uri_addresses = nil
|
|
43
46
|
@resolver = Resolv::DNS.new
|
|
44
|
-
@resolver.timeouts = @
|
|
47
|
+
@resolver.timeouts = @_timeouts.empty? ? Resolver::RESOLVE_TIMEOUT : @_timeouts
|
|
45
48
|
@resolver.lazy_initialize
|
|
46
49
|
end
|
|
47
50
|
|
|
51
|
+
def state
|
|
52
|
+
@resolver_connection ? @resolver_connection.state : :idle
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
def <<(connection)
|
|
49
56
|
return if @uri.origin == connection.peer.to_s
|
|
50
57
|
|
|
51
|
-
@uri_addresses ||=
|
|
58
|
+
@uri_addresses ||= @options.resolver_cache.resolve(@uri.host) || @resolver.getaddresses(@uri.host)
|
|
52
59
|
|
|
53
60
|
if @uri_addresses.empty?
|
|
54
61
|
ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
|
|
@@ -96,21 +103,26 @@ module HTTPX
|
|
|
96
103
|
else
|
|
97
104
|
@queries[hostname] = connection
|
|
98
105
|
end
|
|
106
|
+
|
|
107
|
+
@name = hostname
|
|
108
|
+
|
|
99
109
|
log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
send_request(hostname, connection)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def send_request(hostname, connection)
|
|
115
|
+
request = build_request(hostname)
|
|
116
|
+
request.on(:response, &method(:on_response).curry(2)[request])
|
|
117
|
+
request.on(:promise, &method(:on_promise))
|
|
118
|
+
@requests[request] = hostname
|
|
119
|
+
resolver_connection.send(request)
|
|
120
|
+
@connections << connection
|
|
121
|
+
rescue ResolveError, Resolv::DNS::EncodeError => e
|
|
122
|
+
reset_hostname(hostname)
|
|
123
|
+
throw(:resolve_error, e) if connection.pending.empty?
|
|
124
|
+
emit_resolve_error(connection, connection.peer.host, e)
|
|
125
|
+
close_or_resolve
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
def on_response(request, response)
|
|
@@ -122,6 +134,18 @@ module HTTPX
|
|
|
122
134
|
close_or_resolve
|
|
123
135
|
else
|
|
124
136
|
# @type var response: HTTPX::Response
|
|
137
|
+
if response.status.between?(300, 399) && response.headers.key?("location")
|
|
138
|
+
hostname = @requests[request]
|
|
139
|
+
connection = @queries[hostname]
|
|
140
|
+
location_uri = URI(response.headers["location"])
|
|
141
|
+
location_uri = response.uri.merge(location_uri) if location_uri.relative?
|
|
142
|
+
|
|
143
|
+
# we assume that the DNS server URI changed permanently and move on
|
|
144
|
+
@uri = location_uri
|
|
145
|
+
send_request(hostname, connection)
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
125
149
|
parse(request, response)
|
|
126
150
|
ensure
|
|
127
151
|
@requests.delete(request)
|
|
@@ -133,6 +157,10 @@ module HTTPX
|
|
|
133
157
|
end
|
|
134
158
|
|
|
135
159
|
def parse(request, response)
|
|
160
|
+
hostname = @name
|
|
161
|
+
|
|
162
|
+
@name = nil
|
|
163
|
+
|
|
136
164
|
code, result = decode_response_body(response)
|
|
137
165
|
|
|
138
166
|
case code
|
|
@@ -151,6 +179,23 @@ module HTTPX
|
|
|
151
179
|
end
|
|
152
180
|
|
|
153
181
|
resolve
|
|
182
|
+
when :retriable_error
|
|
183
|
+
timeouts = @timeouts[hostname]
|
|
184
|
+
|
|
185
|
+
unless timeouts.empty?
|
|
186
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
|
|
187
|
+
|
|
188
|
+
connection = @queries[hostname]
|
|
189
|
+
|
|
190
|
+
resolve(connection, hostname)
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
host = @requests.delete(request)
|
|
195
|
+
connection = reset_hostname(host)
|
|
196
|
+
|
|
197
|
+
emit_resolve_error(connection)
|
|
198
|
+
close_or_resolve
|
|
154
199
|
when :dns_error
|
|
155
200
|
host = @requests.delete(request)
|
|
156
201
|
connection = reset_hostname(host)
|
|
@@ -206,7 +251,7 @@ module HTTPX
|
|
|
206
251
|
# eliminate other candidates
|
|
207
252
|
@queries.delete_if { |_, conn| connection == conn }
|
|
208
253
|
|
|
209
|
-
|
|
254
|
+
@options.resolver_cache.set(hostname, @family, addresses) if @resolver_options[:cache]
|
|
210
255
|
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
|
|
211
256
|
end
|
|
212
257
|
end
|
|
@@ -217,15 +262,18 @@ module HTTPX
|
|
|
217
262
|
uri = @uri.dup
|
|
218
263
|
rklass = @options.request_class
|
|
219
264
|
payload = Resolver.encode_dns_query(hostname, type: @record_type)
|
|
265
|
+
timeouts = @timeouts[hostname]
|
|
266
|
+
request_timeout = timeouts.shift
|
|
267
|
+
options = @options.merge(timeout: { request_timeout: request_timeout })
|
|
220
268
|
|
|
221
269
|
if @resolver_options[:use_get]
|
|
222
270
|
params = URI.decode_www_form(uri.query.to_s)
|
|
223
271
|
params << ["type", FAMILY_TYPES[@record_type]]
|
|
224
272
|
params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
|
|
225
273
|
uri.query = URI.encode_www_form(params)
|
|
226
|
-
request = rklass.new("GET", uri,
|
|
274
|
+
request = rklass.new("GET", uri, options)
|
|
227
275
|
else
|
|
228
|
-
request = rklass.new("POST", uri,
|
|
276
|
+
request = rklass.new("POST", uri, options, body: [payload])
|
|
229
277
|
request.headers["content-type"] = "application/dns-message"
|
|
230
278
|
end
|
|
231
279
|
request.headers["accept"] = "application/dns-message"
|
|
@@ -243,6 +291,7 @@ module HTTPX
|
|
|
243
291
|
end
|
|
244
292
|
|
|
245
293
|
def reset_hostname(hostname, reset_candidates: true)
|
|
294
|
+
@timeouts.delete(hostname)
|
|
246
295
|
connection = @queries.delete(hostname)
|
|
247
296
|
|
|
248
297
|
return connection unless connection && reset_candidates
|
|
@@ -250,6 +299,8 @@ module HTTPX
|
|
|
250
299
|
# eliminate other candidates
|
|
251
300
|
candidates = @queries.select { |_, conn| connection == conn }.keys
|
|
252
301
|
@queries.delete_if { |h, _| candidates.include?(h) }
|
|
302
|
+
# reset timeouts
|
|
303
|
+
@timeouts.delete_if { |h, _| candidates.include?(h) }
|
|
253
304
|
|
|
254
305
|
connection
|
|
255
306
|
end
|
|
@@ -259,6 +310,9 @@ module HTTPX
|
|
|
259
310
|
@connections.shift until @connections.empty? || @connections.first.state != :closed
|
|
260
311
|
|
|
261
312
|
if (@connections - @queries.values).empty?
|
|
313
|
+
# the same resolver connection may be serving different https resolvers (AAAA and A).
|
|
314
|
+
return if inflight?
|
|
315
|
+
|
|
262
316
|
if should_deactivate
|
|
263
317
|
deactivate
|
|
264
318
|
else
|
data/lib/httpx/resolver/multi.rb
CHANGED
|
@@ -19,8 +19,10 @@ module HTTPX
|
|
|
19
19
|
resolver.multi = self
|
|
20
20
|
resolver
|
|
21
21
|
end
|
|
22
|
+
end
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
def state
|
|
25
|
+
@resolvers.map(&:state).uniq.join(",")
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def current_selector=(s)
|
|
@@ -43,7 +45,7 @@ module HTTPX
|
|
|
43
45
|
|
|
44
46
|
def early_resolve(connection)
|
|
45
47
|
hostname = connection.peer.host
|
|
46
|
-
addresses = @resolver_options[:cache] && (connection.addresses ||
|
|
48
|
+
addresses = @resolver_options[:cache] && (connection.addresses || nolookup_resolve(hostname, connection.options))
|
|
47
49
|
return false unless addresses
|
|
48
50
|
|
|
49
51
|
ip_families = connection.options.ip_families
|
|
@@ -79,5 +81,11 @@ module HTTPX
|
|
|
79
81
|
@current_session.select_resolver(resolver, @current_selector)
|
|
80
82
|
end
|
|
81
83
|
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def nolookup_resolve(hostname, options)
|
|
88
|
+
options.resolver_cache.resolve(hostname)
|
|
89
|
+
end
|
|
82
90
|
end
|
|
83
91
|
end
|
|
@@ -9,6 +9,7 @@ module HTTPX
|
|
|
9
9
|
#
|
|
10
10
|
class Resolver::Native < Resolver::Resolver
|
|
11
11
|
extend Forwardable
|
|
12
|
+
|
|
12
13
|
using URIExtensions
|
|
13
14
|
|
|
14
15
|
DEFAULTS = {
|
|
@@ -135,7 +136,11 @@ module HTTPX
|
|
|
135
136
|
private
|
|
136
137
|
|
|
137
138
|
def calculate_interests
|
|
138
|
-
|
|
139
|
+
if @queries.empty?
|
|
140
|
+
return @io.interests if (@socket_type == :tcp) && (@state == :idle)
|
|
141
|
+
|
|
142
|
+
return
|
|
143
|
+
end
|
|
139
144
|
|
|
140
145
|
return :r if @write_buffer.empty?
|
|
141
146
|
|
|
@@ -298,16 +303,14 @@ module HTTPX
|
|
|
298
303
|
end
|
|
299
304
|
|
|
300
305
|
def parse(buffer)
|
|
301
|
-
@timer.cancel
|
|
302
|
-
|
|
303
|
-
@timer = @name = nil
|
|
304
|
-
|
|
305
306
|
code, result = Resolver.decode_dns_answer(buffer)
|
|
306
307
|
|
|
307
308
|
case code
|
|
308
309
|
when :ok
|
|
310
|
+
reset_query
|
|
309
311
|
parse_addresses(result)
|
|
310
312
|
when :no_domain_found
|
|
313
|
+
reset_query
|
|
311
314
|
# Indicates no such domain was found.
|
|
312
315
|
hostname, connection = @queries.first
|
|
313
316
|
reset_hostname(hostname, reset_candidates: false)
|
|
@@ -324,6 +327,7 @@ module HTTPX
|
|
|
324
327
|
close_or_resolve
|
|
325
328
|
end
|
|
326
329
|
when :message_truncated
|
|
330
|
+
reset_query
|
|
327
331
|
# TODO: what to do if it's already tcp??
|
|
328
332
|
return if @socket_type == :tcp
|
|
329
333
|
|
|
@@ -332,13 +336,29 @@ module HTTPX
|
|
|
332
336
|
hostname, _ = @queries.first
|
|
333
337
|
reset_hostname(hostname)
|
|
334
338
|
transition(:closed)
|
|
339
|
+
when :retriable_error
|
|
340
|
+
if @name && @timer
|
|
341
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
|
|
342
|
+
return
|
|
343
|
+
end
|
|
344
|
+
# retry now!
|
|
345
|
+
# connection = @queries[@name].shift
|
|
346
|
+
# @timer.fire
|
|
347
|
+
reset_query
|
|
348
|
+
hostname, connection = @queries.first
|
|
349
|
+
reset_hostname(hostname)
|
|
350
|
+
@connections.delete(connection)
|
|
351
|
+
ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
|
|
352
|
+
raise ex
|
|
335
353
|
when :dns_error
|
|
354
|
+
reset_query
|
|
336
355
|
hostname, connection = @queries.first
|
|
337
356
|
reset_hostname(hostname)
|
|
338
357
|
@connections.delete(connection)
|
|
339
358
|
ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
|
|
340
359
|
raise ex
|
|
341
360
|
when :decode_error
|
|
361
|
+
reset_query
|
|
342
362
|
hostname, connection = @queries.first
|
|
343
363
|
reset_hostname(hostname)
|
|
344
364
|
@connections.delete(connection)
|
|
@@ -401,7 +421,7 @@ module HTTPX
|
|
|
401
421
|
reset_hostname(name, connection: connection)
|
|
402
422
|
@timeouts.delete(connection.peer.host)
|
|
403
423
|
@connections.delete(connection)
|
|
404
|
-
|
|
424
|
+
@options.resolver_cache.set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
|
|
405
425
|
catch(:coalesced) do
|
|
406
426
|
emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) })
|
|
407
427
|
end
|
|
@@ -529,6 +549,12 @@ module HTTPX
|
|
|
529
549
|
on_error(e)
|
|
530
550
|
end
|
|
531
551
|
|
|
552
|
+
def reset_query
|
|
553
|
+
@timer.cancel
|
|
554
|
+
|
|
555
|
+
@timer = @name = nil
|
|
556
|
+
end
|
|
557
|
+
|
|
532
558
|
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
|
533
559
|
@timeouts.delete(hostname)
|
|
534
560
|
|
|
@@ -75,7 +75,7 @@ module HTTPX
|
|
|
75
75
|
|
|
76
76
|
# double emission check, but allow early resolution to work
|
|
77
77
|
conn_addrs = connection.addresses
|
|
78
|
-
return if !early_resolve && conn_addrs &&
|
|
78
|
+
return if !early_resolve && conn_addrs && !conn_addrs.empty? && !addresses.intersect?(conn_addrs)
|
|
79
79
|
|
|
80
80
|
log do
|
|
81
81
|
"resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
|
|
@@ -124,8 +124,8 @@ module HTTPX
|
|
|
124
124
|
disconnect
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
def early_resolve(connection, hostname: connection.peer.host)
|
|
128
|
-
addresses = @resolver_options[:cache] && (connection.addresses ||
|
|
127
|
+
def early_resolve(connection, hostname: connection.peer.host) # rubocop:disable Naming/PredicateMethod
|
|
128
|
+
addresses = @resolver_options[:cache] && (connection.addresses || @options.resolver_cache.resolve(hostname))
|
|
129
129
|
|
|
130
130
|
return false unless addresses
|
|
131
131
|
|
data/lib/httpx/resolver.rb
CHANGED
|
@@ -5,132 +5,35 @@ require "resolv"
|
|
|
5
5
|
|
|
6
6
|
module HTTPX
|
|
7
7
|
module Resolver
|
|
8
|
-
|
|
8
|
+
extend self
|
|
9
9
|
|
|
10
|
+
RESOLVE_TIMEOUT = [2, 3].freeze
|
|
10
11
|
require "httpx/resolver/entry"
|
|
12
|
+
require "httpx/resolver/cache"
|
|
11
13
|
require "httpx/resolver/resolver"
|
|
12
14
|
require "httpx/resolver/system"
|
|
13
15
|
require "httpx/resolver/native"
|
|
14
16
|
require "httpx/resolver/https"
|
|
15
17
|
require "httpx/resolver/multi"
|
|
16
18
|
|
|
17
|
-
@lookup_mutex = Thread::Mutex.new
|
|
18
|
-
@lookups = Hash.new { |h, k| h[k] = [] }
|
|
19
|
-
|
|
20
19
|
@identifier_mutex = Thread::Mutex.new
|
|
21
20
|
@identifier = 1
|
|
22
|
-
@hosts_resolver = Resolv::Hosts.new
|
|
23
|
-
|
|
24
|
-
module_function
|
|
25
21
|
|
|
26
22
|
def supported_ip_families
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
[Socket::AF_INET6, Socket::AF_INET]
|
|
32
|
-
else
|
|
33
|
-
[Socket::AF_INET]
|
|
34
|
-
end
|
|
35
|
-
rescue NotImplementedError
|
|
36
|
-
[Socket::AF_INET]
|
|
37
|
-
end.freeze
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def resolver_for(resolver_type, options)
|
|
41
|
-
case resolver_type
|
|
42
|
-
when Symbol
|
|
43
|
-
meth = :"resolver_#{resolver_type}_class"
|
|
44
|
-
|
|
45
|
-
return options.__send__(meth) if options.respond_to?(meth)
|
|
46
|
-
when Class
|
|
47
|
-
return resolver_type if resolver_type < Resolver
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
raise Error, "unsupported resolver type (#{resolver_type})"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def nolookup_resolve(hostname)
|
|
54
|
-
ip_resolve(hostname) || cached_lookup(hostname) || hosts_resolve(hostname)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
|
|
58
|
-
def ip_resolve(hostname)
|
|
59
|
-
[Entry.new(hostname)]
|
|
60
|
-
rescue ArgumentError
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
|
|
64
|
-
# found, or there is no hosts file.
|
|
65
|
-
def hosts_resolve(hostname)
|
|
66
|
-
ips = @hosts_resolver.getaddresses(hostname)
|
|
67
|
-
return if ips.empty?
|
|
68
|
-
|
|
69
|
-
ips.map { |ip| Entry.new(ip) }
|
|
70
|
-
rescue IOError
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def cached_lookup(hostname)
|
|
74
|
-
now = Utils.now
|
|
75
|
-
lookup_synchronize do |lookups|
|
|
76
|
-
lookup(hostname, lookups, now)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def cached_lookup_set(hostname, family, entries)
|
|
81
|
-
lookup_synchronize do |lookups|
|
|
82
|
-
case family
|
|
83
|
-
when Socket::AF_INET6
|
|
84
|
-
lookups[hostname].concat(entries)
|
|
85
|
-
when Socket::AF_INET
|
|
86
|
-
lookups[hostname].unshift(*entries)
|
|
87
|
-
end
|
|
88
|
-
entries.each do |entry|
|
|
89
|
-
next unless entry["name"] != hostname
|
|
90
|
-
|
|
91
|
-
case family
|
|
92
|
-
when Socket::AF_INET6
|
|
93
|
-
lookups[entry["name"]] << entry
|
|
94
|
-
when Socket::AF_INET
|
|
95
|
-
lookups[entry["name"]].unshift(entry)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
23
|
+
if Utils.in_ractor?
|
|
24
|
+
Ractor.store_if_absent(:httpx_supported_ip_families) { find_supported_ip_families }
|
|
25
|
+
else
|
|
26
|
+
@supported_ip_families ||= find_supported_ip_families
|
|
98
27
|
end
|
|
99
28
|
end
|
|
100
29
|
|
|
101
|
-
def cached_lookup_evict(hostname, ip)
|
|
102
|
-
ip = ip.to_s
|
|
103
|
-
|
|
104
|
-
lookup_synchronize do |lookups|
|
|
105
|
-
entries = lookups[hostname]
|
|
106
|
-
|
|
107
|
-
return unless entries
|
|
108
|
-
|
|
109
|
-
lookups.delete_if { |entry| entry["data"] == ip }
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# do not use directly!
|
|
114
|
-
def lookup(hostname, lookups, ttl)
|
|
115
|
-
return unless lookups.key?(hostname)
|
|
116
|
-
|
|
117
|
-
entries = lookups[hostname] = lookups[hostname].select do |address|
|
|
118
|
-
address["TTL"] > ttl
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
ips = entries.flat_map do |address|
|
|
122
|
-
if (als = address["alias"])
|
|
123
|
-
lookup(als, lookups, ttl)
|
|
124
|
-
else
|
|
125
|
-
Entry.new(address["data"], address["TTL"])
|
|
126
|
-
end
|
|
127
|
-
end.compact
|
|
128
|
-
|
|
129
|
-
ips unless ips.empty?
|
|
130
|
-
end
|
|
131
|
-
|
|
132
30
|
def generate_id
|
|
133
|
-
|
|
31
|
+
if Utils.in_ractor?
|
|
32
|
+
identifier = Ractor.store_if_absent(:httpx_resolver_identifier) { -1 }
|
|
33
|
+
Ractor.current[:httpx_resolver_identifier] = (identifier + 1) & 0xFFFF
|
|
34
|
+
else
|
|
35
|
+
id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
|
|
36
|
+
end
|
|
134
37
|
end
|
|
135
38
|
|
|
136
39
|
def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
|
|
@@ -152,7 +55,14 @@ module HTTPX
|
|
|
152
55
|
|
|
153
56
|
return :message_truncated if message.tc == 1
|
|
154
57
|
|
|
155
|
-
|
|
58
|
+
if message.rcode != Resolv::DNS::RCode::NoError
|
|
59
|
+
case message.rcode
|
|
60
|
+
when Resolv::DNS::RCode::ServFail
|
|
61
|
+
return :retriable_error, message.rcode
|
|
62
|
+
else
|
|
63
|
+
return :dns_error, message.rcode
|
|
64
|
+
end
|
|
65
|
+
end
|
|
156
66
|
|
|
157
67
|
addresses = []
|
|
158
68
|
|
|
@@ -178,12 +88,24 @@ module HTTPX
|
|
|
178
88
|
[:ok, addresses]
|
|
179
89
|
end
|
|
180
90
|
|
|
181
|
-
|
|
182
|
-
@lookup_mutex.synchronize { yield(@lookups) }
|
|
183
|
-
end
|
|
91
|
+
private
|
|
184
92
|
|
|
185
93
|
def id_synchronize(&block)
|
|
186
94
|
@identifier_mutex.synchronize(&block)
|
|
187
95
|
end
|
|
96
|
+
|
|
97
|
+
def find_supported_ip_families
|
|
98
|
+
list = Socket.ip_address_list
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
|
|
102
|
+
[Socket::AF_INET6, Socket::AF_INET]
|
|
103
|
+
else
|
|
104
|
+
[Socket::AF_INET]
|
|
105
|
+
end
|
|
106
|
+
rescue NotImplementedError
|
|
107
|
+
[Socket::AF_INET]
|
|
108
|
+
end.freeze
|
|
109
|
+
end
|
|
188
110
|
end
|
|
189
111
|
end
|
data/lib/httpx/response/body.rb
CHANGED
|
@@ -59,13 +59,11 @@ module HTTPX
|
|
|
59
59
|
|
|
60
60
|
chunk = decode_chunk(chunk)
|
|
61
61
|
|
|
62
|
-
size = chunk.bytesize
|
|
63
|
-
@length += size
|
|
64
62
|
transition(:open)
|
|
65
63
|
@buffer.write(chunk)
|
|
66
64
|
|
|
67
65
|
@response.emit(:chunk_received, chunk)
|
|
68
|
-
|
|
66
|
+
chunk.bytesize
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# reads a chunk from the payload (implementation of the IO reader protocol).
|
|
@@ -138,6 +136,8 @@ module HTTPX
|
|
|
138
136
|
else
|
|
139
137
|
IO.copy_stream(@buffer, dest)
|
|
140
138
|
end
|
|
139
|
+
ensure
|
|
140
|
+
close
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# closes/cleans the buffer, resets everything
|
|
@@ -207,6 +207,8 @@ module HTTPX
|
|
|
207
207
|
chunk = inflater.call(chunk)
|
|
208
208
|
end if @inflaters
|
|
209
209
|
|
|
210
|
+
@length += chunk.bytesize
|
|
211
|
+
|
|
210
212
|
chunk
|
|
211
213
|
end
|
|
212
214
|
|
data/lib/httpx/response.rb
CHANGED
|
@@ -211,16 +211,19 @@ module HTTPX
|
|
|
211
211
|
|
|
212
212
|
def initialize(header_value)
|
|
213
213
|
@header_value = header_value
|
|
214
|
+
@mime_type = @charset = nil
|
|
215
|
+
@initialized = false
|
|
214
216
|
end
|
|
215
217
|
|
|
216
218
|
# returns the mime type declared in the header.
|
|
217
219
|
#
|
|
218
220
|
# ContentType.new("application/json; charset=utf-8").mime_type #=> "application/json"
|
|
219
221
|
def mime_type
|
|
220
|
-
return @mime_type if
|
|
222
|
+
return @mime_type if @initialized
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
load
|
|
225
|
+
|
|
226
|
+
@mime_type
|
|
224
227
|
end
|
|
225
228
|
|
|
226
229
|
# returns the charset declared in the header.
|
|
@@ -228,10 +231,23 @@ module HTTPX
|
|
|
228
231
|
# ContentType.new("application/json; charset=utf-8").charset #=> "utf-8"
|
|
229
232
|
# ContentType.new("text/plain").charset #=> nil
|
|
230
233
|
def charset
|
|
231
|
-
return @charset if
|
|
234
|
+
return @charset if @initialized
|
|
235
|
+
|
|
236
|
+
load
|
|
237
|
+
|
|
238
|
+
@charset
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def load
|
|
244
|
+
m = @header_value.to_s[MIME_TYPE_RE, 1]
|
|
245
|
+
m && @mime_type = m.strip.downcase
|
|
246
|
+
|
|
247
|
+
c = @header_value.to_s[CHARSET_RE, 1]
|
|
248
|
+
c && @charset = c.strip.delete('"')
|
|
232
249
|
|
|
233
|
-
|
|
234
|
-
m && @charset = m.strip.delete('"')
|
|
250
|
+
@initialized = true
|
|
235
251
|
end
|
|
236
252
|
end
|
|
237
253
|
|
data/lib/httpx/selector.rb
CHANGED
|
@@ -127,13 +127,15 @@ module HTTPX
|
|
|
127
127
|
private
|
|
128
128
|
|
|
129
129
|
def select(interval, &block)
|
|
130
|
-
has_no_selectables = @selectables.empty?
|
|
131
130
|
# do not cause an infinite loop here.
|
|
132
131
|
#
|
|
133
132
|
# this may happen if timeout calculation actually triggered an error which causes
|
|
134
133
|
# the connections to be reaped (such as the total timeout error) before #select
|
|
135
134
|
# gets called.
|
|
136
|
-
|
|
135
|
+
if @selectables.empty?
|
|
136
|
+
sleep(interval) if interval
|
|
137
|
+
return
|
|
138
|
+
end
|
|
137
139
|
|
|
138
140
|
# @type var r: (selectable | Array[selectable])?
|
|
139
141
|
# @type var w: (selectable | Array[selectable])?
|
|
@@ -171,7 +173,7 @@ module HTTPX
|
|
|
171
173
|
when Array
|
|
172
174
|
select_many(r, w, interval, &block)
|
|
173
175
|
when nil
|
|
174
|
-
return unless interval &&
|
|
176
|
+
return unless interval && @selectables.any?
|
|
175
177
|
|
|
176
178
|
# no selectables
|
|
177
179
|
# TODO: replace with sleep?
|
|
@@ -199,6 +201,12 @@ module HTTPX
|
|
|
199
201
|
def select_many(r, w, interval, &block)
|
|
200
202
|
begin
|
|
201
203
|
readers, writers = ::IO.select(r, w, nil, interval)
|
|
204
|
+
rescue IOError => e
|
|
205
|
+
(Array(r) + Array(w)).each do |sel|
|
|
206
|
+
# TODO: is there a way to cheaply find the IO associated with the error?
|
|
207
|
+
sel.on_error(e)
|
|
208
|
+
sel.force_close(true)
|
|
209
|
+
end
|
|
202
210
|
rescue StandardError => e
|
|
203
211
|
(Array(r) + Array(w)).each do |sel|
|
|
204
212
|
sel.on_error(e)
|
|
@@ -240,6 +248,9 @@ module HTTPX
|
|
|
240
248
|
when :w then io.to_io.wait_writable(interval)
|
|
241
249
|
when :rw then rw_wait(io, interval)
|
|
242
250
|
end
|
|
251
|
+
rescue IOError => e
|
|
252
|
+
io.on_error(e)
|
|
253
|
+
io.force_close(true)
|
|
243
254
|
rescue StandardError => e
|
|
244
255
|
io.on_error(e)
|
|
245
256
|
|