httpx 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/lib/httpx/buffer.rb +2 -0
- data/lib/httpx/chainable.rb +5 -5
- data/lib/httpx/channel/http2.rb +22 -3
- data/lib/httpx/channel.rb +74 -20
- data/lib/httpx/client.rb +33 -23
- data/lib/httpx/connection.rb +77 -9
- data/lib/httpx/errors.rb +4 -0
- data/lib/httpx/io/ssl.rb +19 -1
- data/lib/httpx/io/tcp.rb +25 -7
- data/lib/httpx/io/udp.rb +56 -0
- data/lib/httpx/io/unix.rb +4 -1
- data/lib/httpx/io.rb +2 -0
- data/lib/httpx/options.rb +3 -2
- data/lib/httpx/plugins/proxy.rb +14 -11
- data/lib/httpx/resolver/https.rb +181 -0
- data/lib/httpx/resolver/native.rb +251 -0
- data/lib/httpx/resolver/options.rb +25 -0
- data/lib/httpx/resolver/resolver_mixin.rb +61 -0
- data/lib/httpx/resolver/system.rb +34 -0
- data/lib/httpx/resolver.rb +103 -0
- data/lib/httpx/response.rb +8 -1
- data/lib/httpx/selector.rb +10 -4
- data/lib/httpx/timeout.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd416a44c18c7d00a8257aa0bdb1532c670ef1b9a6b467d21600014544c03df6
|
4
|
+
data.tar.gz: 3b7dce3446530daa9acb1b66b2844ffc88fe50a95e225750d1d83ec052dd4a4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46995eff536a340ec6d70743b650dd7dcb41fcd146308a3ff863980c1b74e8454fa45838b39a94c14a83ca6c6c6d9d8715c51a7bab4a4a969cae0cbf6154d3e8
|
7
|
+
data.tar.gz: 4199024dbaf25064ddb778cabcb8cbd33508ba1715742b774626deda7007ae627aca6523da4c8bbd410b67e7a431754d67f6deb50820953a4420128866319c31
|
data/README.md
CHANGED
@@ -90,6 +90,10 @@ It means that it loads the bare minimum to perform requests, and the user has to
|
|
90
90
|
|
91
91
|
It also means that it ships with the minimum amount of dependencies.
|
92
92
|
|
93
|
+
### DNS-over-HTTPS
|
94
|
+
|
95
|
+
`HTTPX` ships with custom DNS resolver implementations, including a DNS-over-HTTPS resolver.
|
96
|
+
|
93
97
|
## Easy to test
|
94
98
|
|
95
99
|
The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/httpbin/), so there are no mocking/stubbing false positives. The test suite uses [minitest](https://github.com/seattlerb/minitest), but its matchers usage is (almost) limited to `#assert` (`assert` is all you need).
|
data/lib/httpx/buffer.rb
CHANGED
data/lib/httpx/chainable.rb
CHANGED
@@ -32,8 +32,8 @@ module HTTPX
|
|
32
32
|
end
|
33
33
|
alias_method :plugins, :plugin
|
34
34
|
|
35
|
-
def with(options)
|
36
|
-
branch(default_options.merge(options))
|
35
|
+
def with(options, &blk)
|
36
|
+
branch(default_options.merge(options), &blk)
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
@@ -43,9 +43,9 @@ module HTTPX
|
|
43
43
|
end
|
44
44
|
|
45
45
|
# :nodoc:
|
46
|
-
def branch(options)
|
47
|
-
return self.class.new(options) if is_a?(Client)
|
48
|
-
Client.new(options)
|
46
|
+
def branch(options, &blk)
|
47
|
+
return self.class.new(options, &blk) if is_a?(Client)
|
48
|
+
Client.new(options, &blk)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
data/lib/httpx/channel/http2.rb
CHANGED
@@ -7,6 +7,12 @@ module HTTPX
|
|
7
7
|
include Callbacks
|
8
8
|
include Loggable
|
9
9
|
|
10
|
+
Error = Class.new(Error) do
|
11
|
+
def initialize(id, code)
|
12
|
+
super("stream #{id} closed with error: #{code}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
10
16
|
attr_reader :streams, :pending
|
11
17
|
|
12
18
|
def initialize(buffer, options)
|
@@ -157,10 +163,18 @@ module HTTPX
|
|
157
163
|
def on_stream_close(stream, request, error)
|
158
164
|
return handle(request, stream) if request.expects?
|
159
165
|
if error
|
160
|
-
|
166
|
+
ex = Error.new(stream.id, error)
|
167
|
+
ex.set_backtrace(caller)
|
168
|
+
emit(:error, request, ex)
|
161
169
|
else
|
162
170
|
response = request.response
|
163
|
-
|
171
|
+
if response.status == 421
|
172
|
+
ex = MisdirectedRequestError.new(response)
|
173
|
+
ex.set_backtrace(caller)
|
174
|
+
emit(:error, request, ex)
|
175
|
+
else
|
176
|
+
emit(:response, request, response)
|
177
|
+
end
|
164
178
|
end
|
165
179
|
log(level: 2, label: "#{stream.id}: ") { "closing stream" }
|
166
180
|
|
@@ -177,7 +191,12 @@ module HTTPX
|
|
177
191
|
@connection.remote_settings[:settings_max_concurrent_streams]].min
|
178
192
|
end
|
179
193
|
|
180
|
-
def on_close(
|
194
|
+
def on_close(_last_frame, error, _payload)
|
195
|
+
if error
|
196
|
+
ex = Error.new(0, error)
|
197
|
+
ex.set_backtrace(caller)
|
198
|
+
emit(:error, request, ex)
|
199
|
+
end
|
181
200
|
return unless @connection.state == :closed && @connection.active_stream_count.zero?
|
182
201
|
emit(:close)
|
183
202
|
end
|
data/lib/httpx/channel.rb
CHANGED
@@ -50,8 +50,7 @@ module HTTPX
|
|
50
50
|
raise Error, "#{uri}: #{uri.scheme}: unrecognized channel"
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
54
|
-
new(io, options)
|
53
|
+
new(type, uri, options)
|
55
54
|
end
|
56
55
|
end
|
57
56
|
|
@@ -59,8 +58,12 @@ module HTTPX
|
|
59
58
|
|
60
59
|
def_delegator :@write_buffer, :empty?
|
61
60
|
|
62
|
-
|
63
|
-
|
61
|
+
attr_reader :uri, :state
|
62
|
+
|
63
|
+
def initialize(type, uri, options)
|
64
|
+
@type = type
|
65
|
+
@uri = uri
|
66
|
+
@hostnames = [@uri.host]
|
64
67
|
@options = Options.new(options)
|
65
68
|
@window_size = @options.window_size
|
66
69
|
@read_buffer = Buffer.new(BUFFER_SIZE)
|
@@ -70,17 +73,54 @@ module HTTPX
|
|
70
73
|
on(:error) { |ex| on_error(ex) }
|
71
74
|
end
|
72
75
|
|
76
|
+
def addresses=(addrs)
|
77
|
+
@io = IO.registry(@type).new(@uri, addrs, @options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def mergeable?(addresses)
|
81
|
+
return false if @state == :closing || !@io
|
82
|
+
!(@io.addresses & addresses).empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# coalescable channels need to be mergeable!
|
86
|
+
# but internally, #mergeable? is called before #coalescable?
|
87
|
+
def coalescable?(channel)
|
88
|
+
if @io.protocol == "h2" && @uri.scheme == "https"
|
89
|
+
@io.verify_hostname(channel.uri.host)
|
90
|
+
else
|
91
|
+
@uri.host == channel.uri.host &&
|
92
|
+
@uri.port == channel.uri.port &&
|
93
|
+
@uri.scheme == channel.uri.scheme
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def merge(channel)
|
98
|
+
@hostnames += channel.instance_variable_get(:@hostnames)
|
99
|
+
pending = channel.instance_variable_get(:@pending)
|
100
|
+
pending.each do |req, args|
|
101
|
+
send(req, args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def unmerge(channel)
|
106
|
+
@hostnames -= channel.instance_variable_get(:@hostnames)
|
107
|
+
[@parser.pending, @pending].each do |pending|
|
108
|
+
pending.reject! do |request|
|
109
|
+
request.uri == channel.uri && begin
|
110
|
+
request.transition(:idle)
|
111
|
+
channel.send(request)
|
112
|
+
true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
73
118
|
def match?(uri)
|
74
119
|
return false if @state == :closing
|
75
|
-
ips = begin
|
76
|
-
Resolv.getaddresses(uri.host)
|
77
|
-
rescue StandardError
|
78
|
-
[uri.host]
|
79
|
-
end
|
80
120
|
|
81
|
-
|
82
|
-
uri.port == @
|
83
|
-
uri.scheme == @
|
121
|
+
@hostnames.include?(uri.host) &&
|
122
|
+
uri.port == @uri.port &&
|
123
|
+
uri.scheme == @uri.scheme
|
84
124
|
end
|
85
125
|
|
86
126
|
def interests
|
@@ -114,7 +154,9 @@ module HTTPX
|
|
114
154
|
end
|
115
155
|
|
116
156
|
def send(request, **args)
|
117
|
-
if @
|
157
|
+
if @error_response
|
158
|
+
emit(:response, request, @error_response)
|
159
|
+
elsif @parser && !@write_buffer.full?
|
118
160
|
parser.send(request, **args)
|
119
161
|
else
|
120
162
|
@pending << [request, args]
|
@@ -211,8 +253,13 @@ module HTTPX
|
|
211
253
|
end
|
212
254
|
end
|
213
255
|
parser.on(:error) do |request, ex|
|
214
|
-
|
215
|
-
|
256
|
+
case ex
|
257
|
+
when MisdirectedRequestError
|
258
|
+
emit(:uncoalesce, request.uri)
|
259
|
+
else
|
260
|
+
response = ErrorResponse.new(ex, @options)
|
261
|
+
emit(:response, request, response)
|
262
|
+
end
|
216
263
|
end
|
217
264
|
parser
|
218
265
|
end
|
@@ -220,12 +267,14 @@ module HTTPX
|
|
220
267
|
def transition(nextstate)
|
221
268
|
case nextstate
|
222
269
|
# when :idle
|
223
|
-
|
270
|
+
when :idle
|
271
|
+
@error_response = nil
|
224
272
|
when :open
|
225
273
|
return if @state == :closed
|
226
274
|
@io.connect
|
227
275
|
return unless @io.connected?
|
228
276
|
send_pending
|
277
|
+
emit(:open)
|
229
278
|
when :closing
|
230
279
|
return unless @state == :open
|
231
280
|
when :closed
|
@@ -235,9 +284,14 @@ module HTTPX
|
|
235
284
|
@read_buffer.clear
|
236
285
|
end
|
237
286
|
@state = nextstate
|
287
|
+
rescue Errno::EHOSTUNREACH
|
288
|
+
# at this point, all addresses from the IO object have failed
|
289
|
+
reset
|
290
|
+
emit(:unreachable)
|
291
|
+
throw(:jump_tick)
|
238
292
|
rescue Errno::ECONNREFUSED,
|
239
|
-
Errno::ENETUNREACH,
|
240
293
|
Errno::EADDRNOTAVAIL,
|
294
|
+
Errno::EHOSTUNREACH,
|
241
295
|
OpenSSL::SSL::SSLError => e
|
242
296
|
# connect errors, exit gracefully
|
243
297
|
handle_error(e)
|
@@ -251,10 +305,10 @@ module HTTPX
|
|
251
305
|
end
|
252
306
|
|
253
307
|
def handle_error(e)
|
254
|
-
parser.handle_error(e) if parser.respond_to?(:handle_error)
|
255
|
-
|
308
|
+
parser.handle_error(e) if @parser && parser.respond_to?(:handle_error)
|
309
|
+
@error_response = ErrorResponse.new(e, @options)
|
256
310
|
@pending.each do |request, _|
|
257
|
-
emit(:response, request,
|
311
|
+
emit(:response, request, @error_response)
|
258
312
|
end
|
259
313
|
end
|
260
314
|
end
|
data/lib/httpx/client.rb
CHANGED
@@ -56,37 +56,45 @@ module HTTPX
|
|
56
56
|
|
57
57
|
def find_channel(request, **options)
|
58
58
|
uri = URI(request.uri)
|
59
|
-
@connection.find_channel(uri) ||
|
60
|
-
channel = @connection.build_channel(uri, **options)
|
61
|
-
set_channel_callbacks(channel)
|
62
|
-
channel
|
63
|
-
end
|
59
|
+
@connection.find_channel(uri) || build_channel(uri, options)
|
64
60
|
end
|
65
61
|
|
66
|
-
def set_channel_callbacks(channel)
|
62
|
+
def set_channel_callbacks(channel, options)
|
67
63
|
channel.on(:response, &method(:on_response))
|
68
64
|
channel.on(:promise, &method(:on_promise))
|
65
|
+
channel.on(:uncoalesce) do |uncoalesced_uri|
|
66
|
+
other_channel = build_channel(uncoalesced_uri, options)
|
67
|
+
channel.unmerge(other_channel)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_channel(uri, options)
|
72
|
+
channel = @connection.build_channel(uri, **options)
|
73
|
+
set_channel_callbacks(channel, options)
|
74
|
+
channel
|
69
75
|
end
|
70
76
|
|
71
77
|
def __build_reqs(*args, **options)
|
72
|
-
case args.size
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
78
|
+
requests = case args.size
|
79
|
+
when 1
|
80
|
+
reqs = args.first
|
81
|
+
reqs.map do |verb, uri|
|
82
|
+
__build_req(verb, uri, options)
|
83
|
+
end
|
84
|
+
when 2, 3
|
85
|
+
verb, uris = args
|
86
|
+
if uris.respond_to?(:each)
|
87
|
+
uris.map do |uri|
|
88
|
+
__build_req(verb, uri, options)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
[__build_req(verb, uris, options)]
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise ArgumentError, "unsupported number of arguments"
|
89
95
|
end
|
96
|
+
raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty?
|
97
|
+
requests
|
90
98
|
end
|
91
99
|
|
92
100
|
def __send_reqs(*requests, **options)
|
@@ -165,5 +173,7 @@ module HTTPX
|
|
165
173
|
self
|
166
174
|
end
|
167
175
|
end
|
176
|
+
|
177
|
+
plugin(:proxy) unless ENV.grep(/https?_proxy$/i).empty?
|
168
178
|
end
|
169
179
|
end
|
data/lib/httpx/connection.rb
CHANGED
@@ -2,14 +2,21 @@
|
|
2
2
|
|
3
3
|
require "httpx/selector"
|
4
4
|
require "httpx/channel"
|
5
|
+
require "httpx/resolver"
|
5
6
|
|
6
7
|
module HTTPX
|
7
8
|
class Connection
|
8
9
|
def initialize(options)
|
9
10
|
@options = Options.new(options)
|
10
11
|
@timeout = options.timeout
|
12
|
+
resolver_type = @options.resolver_class
|
13
|
+
resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
|
11
14
|
@selector = Selector.new
|
12
15
|
@channels = []
|
16
|
+
@resolver = resolver_type.new(self, @options)
|
17
|
+
@resolver.on(:resolve, &method(:on_resolver_channel))
|
18
|
+
@resolver.on(:error, &method(:on_resolver_error))
|
19
|
+
@resolver.on(:close, &method(:on_resolver_close))
|
13
20
|
end
|
14
21
|
|
15
22
|
def running?
|
@@ -17,12 +24,13 @@ module HTTPX
|
|
17
24
|
end
|
18
25
|
|
19
26
|
def next_tick
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
27
|
+
catch(:jump_tick) do
|
28
|
+
@selector.select(next_timeout) do |monitor|
|
29
|
+
if (channel = monitor.value)
|
30
|
+
channel.call
|
31
|
+
end
|
32
|
+
monitor.interests = channel.interests
|
24
33
|
end
|
25
|
-
monitor.interests = channel.interests
|
26
34
|
end
|
27
35
|
rescue TimeoutError,
|
28
36
|
Errno::ECONNRESET,
|
@@ -34,13 +42,18 @@ module HTTPX
|
|
34
42
|
end
|
35
43
|
|
36
44
|
def close
|
45
|
+
@resolver.close unless @resolver.closed?
|
37
46
|
@channels.each(&:close)
|
38
47
|
next_tick until @channels.empty?
|
39
48
|
end
|
40
49
|
|
41
50
|
def build_channel(uri, **options)
|
42
51
|
channel = Channel.by(uri, @options.merge(options))
|
43
|
-
|
52
|
+
resolve_channel(channel)
|
53
|
+
channel.once(:unreachable) do
|
54
|
+
@resolver.uncache(channel)
|
55
|
+
resolve_channel(channel)
|
56
|
+
end
|
44
57
|
channel
|
45
58
|
end
|
46
59
|
|
@@ -56,14 +69,69 @@ module HTTPX
|
|
56
69
|
|
57
70
|
private
|
58
71
|
|
72
|
+
def resolve_channel(channel)
|
73
|
+
@channels << channel unless @channels.include?(channel)
|
74
|
+
@resolver << channel
|
75
|
+
return if @resolver.empty?
|
76
|
+
@_resolver_monitor ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
|
77
|
+
monitor = @selector.register(@resolver, :w)
|
78
|
+
monitor.value = @resolver
|
79
|
+
monitor
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_resolver_channel(channel, addresses)
|
84
|
+
found_channel = @channels.find do |ch|
|
85
|
+
ch != channel && ch.mergeable?(addresses)
|
86
|
+
end
|
87
|
+
return register_channel(channel) unless found_channel
|
88
|
+
if found_channel.state == :open
|
89
|
+
coalesce_channels(found_channel, channel)
|
90
|
+
else
|
91
|
+
found_channel.once(:open) do
|
92
|
+
coalesce_channels(found_channel, channel)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_resolver_error(ch, error)
|
98
|
+
ch.emit(:error, error)
|
99
|
+
# must remove channel by hand, hasn't been started yet
|
100
|
+
unregister_channel(ch)
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_resolver_close
|
104
|
+
@selector.deregister(@resolver)
|
105
|
+
@_resolver_monitor = nil
|
106
|
+
@resolver.close unless @resolver.closed?
|
107
|
+
end
|
108
|
+
|
59
109
|
def register_channel(channel)
|
60
110
|
monitor = @selector.register(channel, :w)
|
61
111
|
monitor.value = channel
|
62
112
|
channel.on(:close) do
|
63
|
-
|
64
|
-
|
113
|
+
unregister_channel(channel)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def unregister_channel(channel)
|
118
|
+
@channels.delete(channel)
|
119
|
+
@selector.deregister(channel)
|
120
|
+
end
|
121
|
+
|
122
|
+
def next_timeout
|
123
|
+
timeout = @timeout.timeout # force log time
|
124
|
+
return (@resolver.timeout || timeout) unless @resolver.closed?
|
125
|
+
timeout
|
126
|
+
end
|
127
|
+
|
128
|
+
def coalesce_channels(ch1, ch2)
|
129
|
+
if ch1.coalescable?(ch2)
|
130
|
+
ch1.merge(ch2)
|
131
|
+
@channels.delete(ch2)
|
132
|
+
else
|
133
|
+
register_channel(ch2)
|
65
134
|
end
|
66
|
-
@channels << channel
|
67
135
|
end
|
68
136
|
end
|
69
137
|
end
|