httpx 0.3.1 → 0.4.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.
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