httpx 0.1.0 → 0.2.0
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 +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
|