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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34654a12cf778aeffd39429a869bbfb62dc487369ed50fdfc8b14c512e141c91
4
- data.tar.gz: 055b3f79a33ecf43982264f2af5d61631d290aab74c7b63d30fc82501ba366c3
3
+ metadata.gz: dd416a44c18c7d00a8257aa0bdb1532c670ef1b9a6b467d21600014544c03df6
4
+ data.tar.gz: 3b7dce3446530daa9acb1b66b2844ffc88fe50a95e225750d1d83ec052dd4a4b
5
5
  SHA512:
6
- metadata.gz: b9cc0fb05f09c33d2b18f7aa3b46313240744ecb0a8474ffb5345afc2e533768b987609eabd8f1461c00f64815610dcd83e68f340933ce1695ee3efcbc6653a2
7
- data.tar.gz: 21bc5f92e8c8c639a2b0f0885d7652805a099f44a89900da8336993c1e033359245666204d2adccc9fbfb18e49c6dd883fc09ffac5c56b194df056a8cda4b0de
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
@@ -22,6 +22,8 @@ module HTTPX
22
22
 
23
23
  def_delegator :@buffer, :replace
24
24
 
25
+ attr_reader :limit
26
+
25
27
  def initialize(limit)
26
28
  @buffer = "".b
27
29
  @limit = limit
@@ -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
@@ -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
- emit(:error, request, error)
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
- emit(:response, request, response)
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
- io = IO.registry(type).new(uri, options)
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
- def initialize(io, options)
63
- @io = io
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
- ips.include?(@io.ip) &&
82
- uri.port == @io.port &&
83
- uri.scheme == @io.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 @parser && !@write_buffer.full?
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
- response = ErrorResponse.new(ex, @options)
215
- emit(:response, request, response)
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
- response = ErrorResponse.new(e, @options)
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, response)
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) || begin
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
- when 1
74
- reqs = args.first
75
- reqs.map do |verb, uri|
76
- __build_req(verb, uri, options)
77
- end
78
- when 2, 3
79
- verb, uris = args
80
- if uris.respond_to?(:each)
81
- uris.map do |uri|
82
- __build_req(verb, uri, options)
83
- end
84
- else
85
- [__build_req(verb, uris, options)]
86
- end
87
- else
88
- raise ArgumentError, "unsupported number of arguments"
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
@@ -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
- timeout = @timeout.timeout
21
- @selector.select(timeout) do |monitor|
22
- if (channel = monitor.value)
23
- channel.call
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
- register_channel(channel)
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
- @channels.delete(channel)
64
- @selector.deregister(channel)
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