httpx 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/httpx.rb +8 -2
  3. data/lib/httpx/adapters/faraday.rb +203 -0
  4. data/lib/httpx/altsvc.rb +4 -0
  5. data/lib/httpx/callbacks.rb +1 -4
  6. data/lib/httpx/chainable.rb +4 -3
  7. data/lib/httpx/connection.rb +326 -104
  8. data/lib/httpx/{channel → connection}/http1.rb +29 -15
  9. data/lib/httpx/{channel → connection}/http2.rb +12 -6
  10. data/lib/httpx/errors.rb +2 -0
  11. data/lib/httpx/headers.rb +4 -1
  12. data/lib/httpx/io/ssl.rb +5 -1
  13. data/lib/httpx/io/tcp.rb +13 -7
  14. data/lib/httpx/io/udp.rb +1 -0
  15. data/lib/httpx/io/unix.rb +1 -0
  16. data/lib/httpx/loggable.rb +34 -9
  17. data/lib/httpx/options.rb +57 -31
  18. data/lib/httpx/parser/http1.rb +8 -0
  19. data/lib/httpx/plugins/authentication.rb +4 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +4 -0
  21. data/lib/httpx/plugins/compression.rb +22 -5
  22. data/lib/httpx/plugins/cookies.rb +89 -36
  23. data/lib/httpx/plugins/digest_authentication.rb +45 -26
  24. data/lib/httpx/plugins/follow_redirects.rb +61 -62
  25. data/lib/httpx/plugins/h2c.rb +78 -39
  26. data/lib/httpx/plugins/multipart.rb +5 -0
  27. data/lib/httpx/plugins/persistent.rb +29 -0
  28. data/lib/httpx/plugins/proxy.rb +125 -78
  29. data/lib/httpx/plugins/proxy/http.rb +31 -27
  30. data/lib/httpx/plugins/proxy/socks4.rb +30 -24
  31. data/lib/httpx/plugins/proxy/socks5.rb +49 -39
  32. data/lib/httpx/plugins/proxy/ssh.rb +81 -0
  33. data/lib/httpx/plugins/push_promise.rb +18 -9
  34. data/lib/httpx/plugins/retries.rb +43 -15
  35. data/lib/httpx/pool.rb +159 -0
  36. data/lib/httpx/registry.rb +2 -0
  37. data/lib/httpx/request.rb +10 -0
  38. data/lib/httpx/resolver.rb +2 -1
  39. data/lib/httpx/resolver/https.rb +62 -56
  40. data/lib/httpx/resolver/native.rb +48 -37
  41. data/lib/httpx/resolver/resolver_mixin.rb +16 -11
  42. data/lib/httpx/resolver/system.rb +11 -7
  43. data/lib/httpx/response.rb +24 -10
  44. data/lib/httpx/selector.rb +32 -39
  45. data/lib/httpx/{client.rb → session.rb} +99 -62
  46. data/lib/httpx/timeout.rb +7 -15
  47. data/lib/httpx/transcoder/body.rb +4 -0
  48. data/lib/httpx/transcoder/chunker.rb +4 -0
  49. data/lib/httpx/version.rb +1 -1
  50. metadata +10 -8
  51. data/lib/httpx/channel.rb +0 -367
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52b8402031b6deeed1d1a5c92c56e1cc8d93cae1da650d3573ddca712a413948
4
- data.tar.gz: b2c2528c1a187eb0cdfe72636eebf6a390a240e86b78e9ebed2b81f26f3f1c89
3
+ metadata.gz: 853641fa479bca4da2dca4d69bf8eddf6b323b12c40096cdb22faff1a8f80176
4
+ data.tar.gz: 8f74a2b53e2037863b9b66e4184e568f7de33bab9f6a65f27b9b9d8f6f3b145b
5
5
  SHA512:
6
- metadata.gz: '0837f66f56ca695dbc89e9d1557703dba32d74c0748c246f3d94b5fa4baaf13b50a931c8dd018a8c6d4ec80324aac7c164c271be5d9a50afe573c7316e6bf751'
7
- data.tar.gz: ee65a86527d020d422e68d1b5972f11d7ce710267ae4ba31d6bd7d42f6966fa606ddfdf9d4a788175516d2c7a5a857000ab0980d5762667236e8e9e2ef13ce12
6
+ metadata.gz: 736eb3d84089ac6d98576f6aeb1b20f3cb56cf32bc070c999fd41c0b1685612b89cf397884053e8d103c9464c1998f2651b9a219692b36dee9848a051f13d5d4
7
+ data.tar.gz: 6c17fbd06732769f09689ccc2f10e0f5fcb8e897ac6c32aa6e96ef707b2bcc26577b4dae8a8b3486e35495e002e833e06593421f99baa26f3c2586e651aaf393
@@ -12,12 +12,12 @@ require "httpx/registry"
12
12
  require "httpx/transcoder"
13
13
  require "httpx/options"
14
14
  require "httpx/timeout"
15
- require "httpx/connection"
15
+ require "httpx/pool"
16
16
  require "httpx/headers"
17
17
  require "httpx/request"
18
18
  require "httpx/response"
19
19
  require "httpx/chainable"
20
- require "httpx/client"
20
+ require "httpx/session"
21
21
 
22
22
  # Top-Level Namespace
23
23
  #
@@ -47,5 +47,11 @@ module HTTPX
47
47
  end
48
48
  end
49
49
 
50
+ def self.const_missing(const_name)
51
+ super unless const_name == :Client
52
+ warn "DEPRECATION WARNING: the class #{self}::Client is deprecated. Use #{self}::Session instead."
53
+ Session
54
+ end
55
+
50
56
  extend Chainable
51
57
  end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "faraday"
5
+
6
+ module Faraday
7
+ class Adapter
8
+ class HTTPX < Faraday::Adapter
9
+ module RequestMixin
10
+ private
11
+
12
+ def build_request(env)
13
+ meth = env[:method]
14
+
15
+ request_options = {
16
+ headers: env.request_headers,
17
+ body: env.body,
18
+ }
19
+ [meth, env.url, request_options]
20
+ end
21
+ end
22
+
23
+ include RequestMixin
24
+
25
+ class Session < ::HTTPX::Session
26
+ plugin(:compression)
27
+ plugin(:persistent)
28
+
29
+ module ReasonPlugin
30
+ if RUBY_VERSION < "2.5"
31
+ def self.load_dependencies(*)
32
+ require "webrick"
33
+ end
34
+ else
35
+ def self.load_dependencies(*)
36
+ require "net/http/status"
37
+ end
38
+ end
39
+ module ResponseMethods
40
+ if RUBY_VERSION < "2.5"
41
+ def reason
42
+ WEBrick::HTTPStatus::StatusMessage.fetch(@status)
43
+ end
44
+ else
45
+ def reason
46
+ Net::HTTP::STATUS_CODES.fetch(@status)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ plugin(ReasonPlugin)
52
+ end
53
+
54
+ class ParallelManager
55
+ class ResponseHandler
56
+ attr_reader :env
57
+
58
+ def initialize(env)
59
+ @env = env
60
+ end
61
+
62
+ def on_response(&blk)
63
+ if block_given?
64
+ @on_response = lambda do |response|
65
+ blk.call(response)
66
+ end
67
+ self
68
+ else
69
+ @on_response
70
+ end
71
+ end
72
+
73
+ def on_complete(&blk)
74
+ if block_given?
75
+ @on_complete = blk
76
+ self
77
+ else
78
+ @on_complete
79
+ end
80
+ end
81
+
82
+ def respond_to_missing?(meth)
83
+ @env.respond_to?(meth)
84
+ end
85
+
86
+ def method_missing(meth, *args, &blk)
87
+ if @env && @env.respond_to?(meth)
88
+ @env.__send__(meth, *args, &blk)
89
+ else
90
+ super
91
+ end
92
+ end
93
+ end
94
+
95
+ include RequestMixin
96
+
97
+ def initialize
98
+ @session = Session.new
99
+ @handlers = []
100
+ end
101
+
102
+ def enqueue(request)
103
+ handler = ResponseHandler.new(request)
104
+ @handlers << handler
105
+ handler
106
+ end
107
+
108
+ def run
109
+ requests = @handlers.map { |handler| build_request(handler.env) }
110
+ env = @handlers.last.env
111
+
112
+ timeout_options = {
113
+ connect_timeout: env.request.open_timeout,
114
+ operation_timeout: env.request.timeout,
115
+ }.reject { |_, v| v.nil? }
116
+
117
+ options = {
118
+ ssl: env.ssl,
119
+ timeout: timeout_options,
120
+ }
121
+
122
+ proxy_options = { uri: env.request.proxy }
123
+
124
+ session = @session.with(options)
125
+ session = session.plugin(:proxy).with_proxy(proxy_options) if env.request.proxy
126
+
127
+ responses = session.request(requests)
128
+ responses.each_with_index do |response, index|
129
+ handler = @handlers[index]
130
+ handler.on_response.call(response)
131
+ handler.on_complete.call(handler.env)
132
+ end
133
+ end
134
+ end
135
+
136
+ self.supports_parallel = true
137
+
138
+ class << self
139
+ def setup_parallel_manager
140
+ ParallelManager.new
141
+ end
142
+ end
143
+
144
+ def initialize(app)
145
+ super(app)
146
+ @session = Session.new
147
+ end
148
+
149
+ def call(env)
150
+ if parallel?(env)
151
+ handler = env[:parallel_manager].enqueue(env)
152
+ handler.on_response do |response|
153
+ save_response(env, response.status, response.body, response.headers, response.reason) do |response_headers|
154
+ response_headers.merge!(response.headers)
155
+ end
156
+ end
157
+ return handler
158
+ end
159
+
160
+ request_options = build_request(env)
161
+
162
+ timeout_options = {
163
+ connect_timeout: env.request.open_timeout,
164
+ operation_timeout: env.request.timeout,
165
+ }.reject { |_, v| v.nil? }
166
+
167
+ options = {
168
+ ssl: env.ssl,
169
+ timeout: timeout_options,
170
+ }
171
+
172
+ proxy_options = { uri: env.request.proxy }
173
+
174
+ session = @session.with(options)
175
+ session = session.plugin(:proxy).with_proxy(proxy_options) if env.request.proxy
176
+ response = session.__send__(*request_options)
177
+ response.raise_for_status unless response.is_a?(::HTTPX::Response)
178
+ save_response(env, response.status, response.body, response.headers, response.reason) do |response_headers|
179
+ response_headers.merge!(response.headers)
180
+ end
181
+ @app.call(env)
182
+ rescue OpenSSL::SSL::SSLError => err
183
+ raise Error::SSLError, err
184
+ rescue Errno::ECONNABORTED,
185
+ Errno::ECONNREFUSED,
186
+ Errno::ECONNRESET,
187
+ Errno::EHOSTUNREACH,
188
+ Errno::EINVAL,
189
+ Errno::ENETUNREACH,
190
+ Errno::EPIPE => err
191
+ raise Error::ConnectionFailed, err
192
+ end
193
+
194
+ private
195
+
196
+ def parallel?(env)
197
+ env[:parallel_manager]
198
+ end
199
+ end
200
+
201
+ register_middleware httpx: HTTPX
202
+ end
203
+ end
@@ -18,6 +18,7 @@ module HTTPX
18
18
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
19
  @altsvc_mutex.synchronize do
20
20
  return if @altsvcs[origin].any? { |altsvc| altsvc["origin"] == entry["origin"] }
21
+
21
22
  entry["TTL"] = Integer(entry["ma"]) + now if entry.key?("ma")
22
23
  @altsvcs[origin] << entry
23
24
  entry
@@ -26,6 +27,7 @@ module HTTPX
26
27
 
27
28
  def lookup(origin, ttl)
28
29
  return [] unless @altsvcs.key?(origin)
30
+
29
31
  @altsvcs[origin] = @altsvcs[origin].select do |entry|
30
32
  !entry.key?("TTL") || entry["TTL"] > ttl
31
33
  end
@@ -35,6 +37,7 @@ module HTTPX
35
37
  def emit(request, response)
36
38
  # Alt-Svc
37
39
  return unless response.headers.key?("alt-svc")
40
+
38
41
  origin = request.origin
39
42
  host = request.uri.host
40
43
  parse(response.headers["alt-svc"]) do |alt_origin, alt_params|
@@ -45,6 +48,7 @@ module HTTPX
45
48
 
46
49
  def parse(altsvc)
47
50
  return enum_for(__method__, altsvc) unless block_given?
51
+
48
52
  alt_origins, *alt_params = altsvc.split(/ *; */)
49
53
  alt_params = Hash[alt_params.map { |field| field.split("=") }]
50
54
  alt_origins.split(/ *, */).each do |alt_origin|
@@ -19,12 +19,9 @@ module HTTPX
19
19
 
20
20
  protected
21
21
 
22
- def inherit_callbacks(callbackable)
23
- @callbacks = callbackable.callbacks
24
- end
25
-
26
22
  def callbacks(type = nil)
27
23
  return @callbacks unless type
24
+
28
25
  @callbacks ||= Hash.new { |h, k| h[k] = [] }
29
26
  @callbacks[type]
30
27
  end
@@ -29,7 +29,7 @@ module HTTPX
29
29
  end
30
30
 
31
31
  def plugin(*plugins)
32
- klass = is_a?(Client) ? self.class : Client
32
+ klass = is_a?(Session) ? self.class : Session
33
33
  klass = Class.new(klass)
34
34
  klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
35
35
  klass.plugins(plugins).new
@@ -48,8 +48,9 @@ module HTTPX
48
48
 
49
49
  # :nodoc:
50
50
  def branch(options, &blk)
51
- return self.class.new(options, &blk) if is_a?(Client)
52
- Client.new(options, &blk)
51
+ return self.class.new(options, &blk) if is_a?(Session)
52
+
53
+ Session.new(options, &blk)
53
54
  end
54
55
  end
55
56
  end
@@ -1,150 +1,372 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "httpx/selector"
4
- require "httpx/channel"
5
- require "httpx/resolver"
3
+ require "resolv"
4
+ require "forwardable"
5
+ require "httpx/io"
6
+ require "httpx/buffer"
6
7
 
7
8
  module HTTPX
9
+ # The Connection can be watched for IO events.
10
+ #
11
+ # It contains the +io+ object to read/write from, and knows what to do when it can.
12
+ #
13
+ # It defers connecting until absolutely necessary. Connection should be triggered from
14
+ # the IO selector (until then, any request will be queued).
15
+ #
16
+ # A connection boots up its parser after connection is established. All pending requests
17
+ # will be redirected there after connection.
18
+ #
19
+ # A connection can be prevented from closing by the parser, that is, if there are pending
20
+ # requests. This will signal that the connection was prematurely closed, due to a possible
21
+ # number of conditions:
22
+ #
23
+ # * Remote peer closed the connection ("Connection: close");
24
+ # * Remote peer doesn't support pipelining;
25
+ #
26
+ # A connection may also route requests for a different host for which the +io+ was connected
27
+ # to, provided that the IP is the same and the port and scheme as well. This will allow to
28
+ # share the same socket to send HTTP/2 requests to different hosts.
29
+ #
8
30
  class Connection
9
- def initialize(options)
31
+ extend Forwardable
32
+ include Registry
33
+ include Loggable
34
+ include Callbacks
35
+
36
+ using URIExtensions
37
+
38
+ require "httpx/connection/http2"
39
+ require "httpx/connection/http1"
40
+
41
+ BUFFER_SIZE = 1 << 14
42
+
43
+ def_delegator :@io, :closed?
44
+
45
+ def_delegator :@write_buffer, :empty?
46
+
47
+ attr_reader :origin, :state, :pending, :options
48
+
49
+ attr_reader :timeout
50
+
51
+ def initialize(type, uri, options)
52
+ @type = type
53
+ @origins = [uri.origin]
54
+ @origin = URI(uri.origin)
10
55
  @options = Options.new(options)
11
- @timeout = options.timeout
12
- resolver_type = @options.resolver_class
13
- resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
14
- @selector = Selector.new
15
- @channels = []
16
- @connected_channels = 0
17
- @resolver = resolver_type.new(self, @options)
18
- @resolver.on(:resolve, &method(:on_resolver_channel))
19
- @resolver.on(:error, &method(:on_resolver_error))
20
- @resolver.on(:close, &method(:on_resolver_close))
21
- end
22
-
23
- def running?
24
- !@channels.empty?
25
- end
26
-
27
- def next_tick
28
- catch(:jump_tick) do
29
- @selector.select(next_timeout) do |monitor|
30
- if (channel = monitor.value)
31
- channel.call
32
- end
33
- monitor.interests = channel.interests
56
+ @window_size = @options.window_size
57
+ @read_buffer = Buffer.new(BUFFER_SIZE)
58
+ @write_buffer = Buffer.new(BUFFER_SIZE)
59
+ @pending = []
60
+ on(:error, &method(:on_error))
61
+ if @options.io
62
+ # if there's an already open IO, get its
63
+ # peer address, and force-initiate the parser
64
+ transition(:already_open)
65
+ @io = IO.registry(@type).new(@origin, nil, @options)
66
+ parser
67
+ else
68
+ transition(:idle)
69
+ end
70
+ end
71
+
72
+ # this is a semi-private method, to be used by the resolver
73
+ # to initiate the io object.
74
+ def addresses=(addrs)
75
+ @io ||= IO.registry(@type).new(@origin, addrs, @options) # rubocop:disable Naming/MemoizedInstanceVariableName
76
+ end
77
+
78
+ def addresses
79
+ @io && @io.addresses
80
+ end
81
+
82
+ def match?(uri, options)
83
+ return false if @state == :closing || @state == :closed
84
+
85
+ (@origins.include?(uri.origin) || match_altsvcs?(uri)) && @options == options
86
+ end
87
+
88
+ def mergeable?(connection)
89
+ return false if @state == :closing || @state == :closed || !@io
90
+
91
+ !(@io.addresses & connection.addresses).empty? && @options == connection.options
92
+ end
93
+
94
+ # coalescable connections need to be mergeable!
95
+ # but internally, #mergeable? is called before #coalescable?
96
+ def coalescable?(connection)
97
+ if @io.protocol == "h2" && @origin.scheme == "https"
98
+ @io.verify_hostname(connection.origin.host)
99
+ else
100
+ @origin == connection.origin
101
+ end
102
+ end
103
+
104
+ def merge(connection)
105
+ @origins += connection.instance_variable_get(:@origins)
106
+ pending = connection.instance_variable_get(:@pending)
107
+ pending.each do |req, args|
108
+ send(req, args)
109
+ end
110
+ end
111
+
112
+ def unmerge(connection)
113
+ @origins -= connection.instance_variable_get(:@origins)
114
+ purge_pending do |request|
115
+ request.uri.origin == connection.origin && begin
116
+ request.transition(:idle)
117
+ connection.send(request)
118
+ true
34
119
  end
35
120
  end
36
- rescue TimeoutError => timeout_error
37
- @channels.each do |ch|
38
- ch.handle_timeout_error(timeout_error)
121
+ end
122
+
123
+ def purge_pending
124
+ [@parser.pending, @pending].each do |pending|
125
+ pending.reject! do |request, *args|
126
+ yield(request, args)
127
+ end
39
128
  end
40
- rescue Errno::ECONNRESET,
41
- Errno::ECONNABORTED,
42
- Errno::EPIPE => ex
43
- @channels.each do |ch|
44
- ch.emit(:error, ex)
129
+ end
130
+
131
+ # checks if this is connection is an alternative service of
132
+ # +uri+
133
+ def match_altsvcs?(uri)
134
+ AltSvc.cached_altsvc(@origin).any? do |altsvc|
135
+ origin = altsvc["origin"]
136
+ origin.altsvc_match?(uri.origin)
45
137
  end
46
138
  end
47
139
 
48
- def close
49
- @resolver.close unless @resolver.closed?
50
- @channels.each(&:close)
51
- next_tick until @channels.empty?
140
+ def connecting?
141
+ @state == :idle
52
142
  end
53
143
 
54
- def build_channel(uri, **options)
55
- channel = Channel.by(uri, @options.merge(options))
56
- resolve_channel(channel)
57
- channel.on(:open) do
58
- @connected_channels += 1
59
- @timeout.transition(:open) if @channels.size == @connected_channels
144
+ def inflight?
145
+ @parser && !@parser.empty? && !@write_buffer.empty?
146
+ end
147
+
148
+ def interests
149
+ return :w if @state == :idle
150
+
151
+ readable = !@read_buffer.full?
152
+ writable = !@write_buffer.empty?
153
+ if readable
154
+ writable ? :rw : :r
155
+ else
156
+ writable ? :w : :r
60
157
  end
61
- channel.on(:reset) do
62
- @timeout.transition(:idle)
158
+ end
159
+
160
+ def to_io
161
+ case @state
162
+ when :idle
163
+ transition(:open)
63
164
  end
64
- channel.on(:unreachable) do
65
- @resolver.uncache(channel)
66
- resolve_channel(channel)
165
+ @io.to_io
166
+ end
167
+
168
+ def close
169
+ @parser.close if @parser
170
+ transition(:closing)
171
+ end
172
+
173
+ def reset
174
+ transition(:closing)
175
+ transition(:closed)
176
+ emit(:close)
177
+ end
178
+
179
+ def send(request)
180
+ if @error_response
181
+ emit(:response, request, @error_response)
182
+ elsif @parser && !@write_buffer.full?
183
+ request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
184
+ parser.send(request)
185
+ else
186
+ @pending << request
67
187
  end
68
- channel
69
188
  end
70
189
 
71
- # opens a channel to the IP reachable through +uri+.
72
- # Many hostnames are reachable through the same IP, so we try to
73
- # maximize pipelining by opening as few channels as possible.
74
- #
75
- def find_channel(uri)
76
- @channels.find do |channel|
77
- channel.match?(uri)
190
+ def call
191
+ @timeout = @timeout_threshold
192
+ case @state
193
+ when :closed
194
+ return
195
+ when :closing
196
+ dwrite
197
+ transition(:closed)
198
+ emit(:close)
199
+ when :open
200
+ consume
78
201
  end
202
+ nil
79
203
  end
80
204
 
81
205
  private
82
206
 
83
- def resolve_channel(channel)
84
- @channels << channel unless @channels.include?(channel)
85
- @resolver << channel
86
- return if @resolver.empty?
87
- @_resolver_monitor ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
88
- monitor = @selector.register(@resolver, :w)
89
- monitor.value = @resolver
90
- monitor
207
+ def consume
208
+ catch(:called) do
209
+ dread
210
+ dwrite
211
+ parser.consume
91
212
  end
92
213
  end
93
214
 
94
- def on_resolver_channel(channel, addresses)
95
- found_channel = @channels.find do |ch|
96
- ch != channel && ch.mergeable?(addresses)
215
+ def dread(wsize = @window_size)
216
+ loop do
217
+ siz = @io.read(wsize, @read_buffer)
218
+ unless siz
219
+ ex = EOFError.new("descriptor closed")
220
+ ex.set_backtrace(caller)
221
+ on_error(ex)
222
+ return
223
+ end
224
+ return if siz.zero?
225
+
226
+ log { "READ: #{siz} bytes..." }
227
+ parser << @read_buffer.to_s
228
+ return if @state == :closing || @state == :closed
97
229
  end
98
- return register_channel(channel) unless found_channel
99
- if found_channel.state == :open
100
- coalesce_channels(found_channel, channel)
101
- else
102
- found_channel.once(:open) do
103
- coalesce_channels(found_channel, channel)
230
+ end
231
+
232
+ def dwrite
233
+ loop do
234
+ return if @write_buffer.empty?
235
+
236
+ siz = @io.write(@write_buffer)
237
+ unless siz
238
+ ex = EOFError.new("descriptor closed")
239
+ ex.set_backtrace(caller)
240
+ on_error(ex)
241
+ return
104
242
  end
243
+ log { "WRITE: #{siz} bytes..." }
244
+ return if siz.zero?
245
+ return if @state == :closing || @state == :closed
105
246
  end
106
247
  end
107
248
 
108
- def on_resolver_error(ch, error)
109
- ch.emit(:error, error)
110
- # must remove channel by hand, hasn't been started yet
111
- unregister_channel(ch)
249
+ def send_pending
250
+ while !@write_buffer.full? && (req_args = @pending.shift)
251
+ request = req_args
252
+ parser.send(request)
253
+ end
112
254
  end
113
255
 
114
- def on_resolver_close
115
- @selector.deregister(@resolver)
116
- @_resolver_monitor = nil
117
- @resolver.close unless @resolver.closed?
256
+ def parser
257
+ @parser ||= build_parser
118
258
  end
119
259
 
120
- def register_channel(channel)
121
- @timeout.transition(:idle)
122
- monitor = @selector.register(channel, :w)
123
- monitor.value = channel
124
- channel.on(:close) do
125
- unregister_channel(channel)
126
- end
260
+ def build_parser(protocol = @io.protocol)
261
+ parser = registry(protocol).new(@write_buffer, @options)
262
+ set_parser_callbacks(parser)
263
+ parser
127
264
  end
128
265
 
129
- def unregister_channel(channel)
130
- @channels.delete(channel)
131
- @selector.deregister(channel)
132
- @connected_channels -= 1
266
+ def set_parser_callbacks(parser)
267
+ parser.on(:response) do |request, response|
268
+ AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
269
+ emit(:altsvc, alt_origin, origin, alt_params)
270
+ end
271
+ request.emit(:response, response)
272
+ end
273
+ parser.on(:altsvc) do |alt_origin, origin, alt_params|
274
+ emit(:altsvc, alt_origin, origin, alt_params)
275
+ end
276
+
277
+ parser.on(:promise) do |request, stream|
278
+ request.emit(:promise, parser, stream)
279
+ end
280
+ parser.on(:close) do
281
+ transition(:closing)
282
+ end
283
+ parser.on(:reset) do
284
+ transition(:closing)
285
+ unless parser.empty?
286
+ transition(:closed)
287
+ emit(:reset)
288
+ transition(:idle)
289
+ transition(:open)
290
+ end
291
+ end
292
+ parser.on(:timeout) do |timeout|
293
+ @timeout = timeout
294
+ end
295
+ parser.on(:error) do |request, ex|
296
+ case ex
297
+ when MisdirectedRequestError
298
+ emit(:uncoalesce, request.uri)
299
+ else
300
+ response = ErrorResponse.new(ex, @options)
301
+ request.emit(:response, response)
302
+ end
303
+ end
133
304
  end
134
305
 
135
- def coalesce_channels(ch1, ch2)
136
- if ch1.coalescable?(ch2)
137
- ch1.merge(ch2)
138
- @channels.delete(ch2)
139
- else
140
- register_channel(ch2)
306
+ def transition(nextstate)
307
+ case nextstate
308
+ when :idle
309
+ @error_response = nil
310
+ @timeout_threshold = @options.timeout.connect_timeout
311
+ @timeout = @timeout_threshold
312
+ when :open
313
+ return if @state == :closed
314
+
315
+ @io.connect
316
+ return unless @io.connected?
317
+
318
+ send_pending
319
+ @timeout_threshold = @options.timeout.operation_timeout
320
+ @timeout = @timeout_threshold
321
+ emit(:open)
322
+ when :closing
323
+ return unless @state == :open
324
+ when :closed
325
+ return unless @state == :closing
326
+ return unless @write_buffer.empty?
327
+
328
+ @io.close
329
+ @read_buffer.clear
330
+ when :already_open
331
+ nextstate = :open
332
+ send_pending
333
+ @timeout_threshold = @options.timeout.operation_timeout
334
+ @timeout = @timeout_threshold
141
335
  end
336
+ @state = nextstate
337
+ rescue Errno::EHOSTUNREACH
338
+ # at this point, all addresses from the IO object have failed
339
+ reset
340
+ emit(:unreachable)
341
+ throw(:jump_tick)
342
+ rescue Errno::ECONNREFUSED,
343
+ Errno::EADDRNOTAVAIL,
344
+ Errno::EHOSTUNREACH,
345
+ OpenSSL::SSL::SSLError => e
346
+ # connect errors, exit gracefully
347
+ handle_error(e)
348
+ @state = :closed
349
+ emit(:close)
350
+ end
351
+
352
+ def on_error(ex)
353
+ handle_error(ex)
354
+ reset
142
355
  end
143
356
 
144
- def next_timeout
145
- timeout = @timeout.timeout
146
- return (@resolver.timeout || timeout) unless @resolver.closed?
147
- timeout
357
+ def handle_error(e)
358
+ if e.instance_of?(TimeoutError) && @timeout
359
+ @timeout -= e.timeout
360
+ return unless @timeout <= 0
361
+
362
+ e = e.to_connection_error if connecting?
363
+ end
364
+
365
+ parser.handle_error(e) if @parser && parser.respond_to?(:handle_error)
366
+ @error_response = ErrorResponse.new(e, @options)
367
+ @pending.each do |request, _|
368
+ request.emit(:response, @error_response)
369
+ end
148
370
  end
149
371
  end
150
372
  end