httpx 1.1.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/release_notes/1_2_0.md +49 -0
  4. data/lib/httpx/adapters/webmock.rb +25 -3
  5. data/lib/httpx/altsvc.rb +57 -2
  6. data/lib/httpx/chainable.rb +40 -29
  7. data/lib/httpx/connection/http1.rb +27 -22
  8. data/lib/httpx/connection/http2.rb +7 -3
  9. data/lib/httpx/connection.rb +45 -60
  10. data/lib/httpx/extensions.rb +0 -15
  11. data/lib/httpx/options.rb +84 -27
  12. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  13. data/lib/httpx/plugins/basic_auth.rb +1 -1
  14. data/lib/httpx/plugins/callbacks.rb +91 -0
  15. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  16. data/lib/httpx/plugins/cookies.rb +19 -9
  17. data/lib/httpx/plugins/digest_auth.rb +1 -1
  18. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  19. data/lib/httpx/plugins/grpc.rb +2 -2
  20. data/lib/httpx/plugins/h2c.rb +20 -8
  21. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  22. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  23. data/lib/httpx/plugins/proxy.rb +14 -32
  24. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  25. data/lib/httpx/plugins/retries.rb +4 -0
  26. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  27. data/lib/httpx/plugins/stream.rb +1 -1
  28. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  29. data/lib/httpx/plugins/upgrade.rb +1 -1
  30. data/lib/httpx/plugins/webdav.rb +1 -1
  31. data/lib/httpx/pool.rb +32 -28
  32. data/lib/httpx/request/body.rb +3 -3
  33. data/lib/httpx/request.rb +3 -5
  34. data/lib/httpx/resolver/https.rb +3 -2
  35. data/lib/httpx/resolver/native.rb +1 -0
  36. data/lib/httpx/resolver/resolver.rb +17 -6
  37. data/lib/httpx/session.rb +13 -82
  38. data/lib/httpx/timers.rb +3 -10
  39. data/lib/httpx/transcoder.rb +1 -1
  40. data/lib/httpx/version.rb +1 -1
  41. data/sig/altsvc.rbs +33 -0
  42. data/sig/chainable.rbs +1 -0
  43. data/sig/connection/http1.rbs +2 -1
  44. data/sig/connection.rbs +16 -16
  45. data/sig/options.rbs +10 -2
  46. data/sig/plugins/callbacks.rbs +38 -0
  47. data/sig/plugins/cookies.rbs +2 -0
  48. data/sig/plugins/follow_redirects.rbs +2 -0
  49. data/sig/plugins/proxy/socks4.rbs +2 -1
  50. data/sig/plugins/proxy/socks5.rbs +2 -1
  51. data/sig/plugins/proxy.rbs +11 -1
  52. data/sig/pool.rbs +1 -3
  53. data/sig/resolver/resolver.rbs +3 -1
  54. data/sig/session.rbs +4 -4
  55. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d95b9f470645015a3308ade4ab2349375eddd0598f6919fbf063d4feba61926c
4
- data.tar.gz: 95518fc5601eb0ba9a22e13814f9f0a339addca71e94c33122ed211780049ca4
3
+ metadata.gz: f3df88a59256b85aacf9d98578c965626b3bfa4611695ae21a707252df71cb0a
4
+ data.tar.gz: f1616869724c217272d0dc165130548edc8ca35eadd97ab42d5a17012a0019d6
5
5
  SHA512:
6
- metadata.gz: 729bfc7fc5888f6872ecf9dfdcaefa6df68ba96b705cf1ccd1e8b9b866d25bf2c3081577b3c1828b99556009fc686c0705eb30dcb6fd5f2a3fb951128e0c621e
7
- data.tar.gz: 1173ae3cd242caf251c099e14db090efc3ac3524d8e3f63e04c331f2938a6d747c9874999d79dbc4790894fc8c447c900740235a62cb0376f2b124dad3674acc
6
+ metadata.gz: b4ca8efc0a1ffe3dec2d2d6ee345bcb58bc2eec9e4c5b4fe951a889041be8eeaeb4b005d9771f0e712f161dad69f8de25d07d520c3e0cd17ae1574f53fadace1
7
+ data.tar.gz: 509c48d74aafb520e3142c5f9b2356e2f36bf9d649eb723ca07a2863077057ab12ae2e17dbf61f13149eac4f80201252dad9b46cb904f5664b8910b62a868c1d
data/README.md CHANGED
@@ -61,7 +61,7 @@ puts body #=> #<HTTPX::Response ...
61
61
  You can also send as many requests as you want simultaneously:
62
62
 
63
63
  ```ruby
64
- page1, page2, page3 =`HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
64
+ page1, page2, page3 = HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
65
65
  ```
66
66
 
67
67
  ## Installation
@@ -107,22 +107,22 @@ HTTPX.get(
107
107
 
108
108
  ```ruby
109
109
  response = HTTPX.get("https://www.google.com", params: { q: "me" })
110
- response = HTTPX.post("https://www.nghttp2.org/httpbin/post", form: {name: "John", age: "22"})
110
+ response = HTTPX.post("https://www.nghttp2.org/httpbin/post", form: { name: "John", age: "22" })
111
111
  response = HTTPX.plugin(:basic_auth)
112
112
  .basic_auth("user", "pass")
113
113
  .get("https://www.google.com")
114
114
 
115
115
  # more complex client objects can be cached, and are thread-safe
116
- http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
116
+ http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN" })
117
117
  http.get("https://example.com") # the above options will apply
118
- http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
118
+ http.post("https://example2.com", form: { name: "John", age: "22" }) # same, plus the form POST body
119
119
  ```
120
120
 
121
121
  ### Lightweight
122
122
 
123
123
  It ships with most features published as a plugin, making vanilla `httpx` lightweight and dependency-free, while allowing you to "pay for what you use"
124
124
 
125
- The plugin system is similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/janko-m/shrine).
125
+ The plugin system is similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/shrinerb/shrine).
126
126
 
127
127
  ### Advanced DNS features
128
128
 
@@ -0,0 +1,49 @@
1
+ # 1.2.0
2
+
3
+ ## Features
4
+
5
+ ### `:ssrf_filter` plugin
6
+
7
+ The `:ssrf_filter` p plugin prevents server-side request forgery attacks, by blocking requests to the internal network. This is useful when the URLs used to perform requests aren’t under the developer control (such as when they are inserted via a web application form).
8
+
9
+ ```ruby
10
+ http = HTTPX.plugin(:ssrf_filter)
11
+
12
+ # this works
13
+ response = http.get("https://example.com")
14
+
15
+ # this doesn't
16
+ response = http.get("http://localhost:3002")
17
+ response = http.get("http://[::1]:3002")
18
+ response = http.get("http://169.254.169.254/latest/meta-data/")
19
+ ```
20
+
21
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/SSRF-Filter
22
+
23
+ ### `:callbacks` plugin
24
+
25
+ The session callbacks introduced in v0.24.0 are in its own plugin. Older code will still work and emit a deprecation warning.
26
+
27
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/Callbacks
28
+
29
+ ### `:redirect_on` option for `:follow_redirects` plugin
30
+
31
+ This option allows passing a callback which, when returning `false`, can interrupt the redirect loop.
32
+
33
+ ```ruby
34
+ http = HTTPX.plugin(:follow_redirects).with(redirect_on: ->(location_uri) { BLACKLIST_HOSTS.include?(location_uri.host) ]
35
+ ```
36
+
37
+ ### `:close_on_handshake_timeout` timeout
38
+
39
+ A new `:timeout` option, `:close_handshake_timeout`, is added, which monitors connection readiness when performing HTTP/2 connection termination handshake.
40
+
41
+ ## Improvements
42
+
43
+ * Internal "eden connections" concept was removed, and connection objects are now kept-and-reused during the lifetime of a session, even when closed. This simplified connectio pool implementation and improved performance.
44
+ * request using `:proxy` and `:retries` plugin enabled sessions will now retry on proxy connection establishment related errors.
45
+
46
+ ## Bugfixes
47
+
48
+ * webmock adapter: mocked responses storing decoded payloads won't try to decode them again (fixes vcr/webmock integrations).
49
+ * webmock adapter: fix issue related with making real requests over webmock-enabled connection.
@@ -41,7 +41,9 @@ module WebMock
41
41
  request.options.response_class.new(request,
42
42
  webmock_response.status[0],
43
43
  "2.0",
44
- webmock_response.headers)
44
+ webmock_response.headers).tap do |res|
45
+ res.mocked = true
46
+ end
45
47
  end
46
48
 
47
49
  def build_error_response(request, exception)
@@ -50,16 +52,36 @@ module WebMock
50
52
  end
51
53
 
52
54
  module InstanceMethods
53
- def build_connection(*)
55
+ def init_connection(*)
54
56
  connection = super
55
57
  connection.once(:unmock_connection) do
58
+ unless connection.addresses
59
+ connection.__send__(:callbacks)[:connect_error].clear
60
+ pool.__send__(:unregister_connection, connection)
61
+ end
56
62
  pool.__send__(:resolve_connection, connection)
57
- pool.__send__(:unregister_connection, connection) unless connection.addresses
58
63
  end
59
64
  connection
60
65
  end
61
66
  end
62
67
 
68
+ module ResponseMethods
69
+ attr_accessor :mocked
70
+
71
+ def initialize(*)
72
+ super
73
+ @mocked = false
74
+ end
75
+ end
76
+
77
+ module ResponseBodyMethods
78
+ def decode_chunk(chunk)
79
+ return chunk if @response.mocked
80
+
81
+ super
82
+ end
83
+ end
84
+
63
85
  module ConnectionMethods
64
86
  def initialize(*)
65
87
  super
data/lib/httpx/altsvc.rb CHANGED
@@ -4,6 +4,58 @@ require "strscan"
4
4
 
5
5
  module HTTPX
6
6
  module AltSvc
7
+ # makes connections able to accept requests destined to primary service.
8
+ module ConnectionMixin
9
+ using URIExtensions
10
+
11
+ def send(request)
12
+ request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
13
+
14
+ super
15
+ end
16
+
17
+ def match?(uri, options)
18
+ return false if !used? && (@state == :closing || @state == :closed)
19
+
20
+ match_altsvcs?(uri) && match_altsvc_options?(uri, options)
21
+ end
22
+
23
+ private
24
+
25
+ # checks if this is connection is an alternative service of
26
+ # +uri+
27
+ def match_altsvcs?(uri)
28
+ @origins.any? { |origin| altsvc_match?(uri, origin) } ||
29
+ AltSvc.cached_altsvc(@origin).any? do |altsvc|
30
+ origin = altsvc["origin"]
31
+ altsvc_match?(origin, uri.origin)
32
+ end
33
+ end
34
+
35
+ def match_altsvc_options?(uri, options)
36
+ return @options == options unless @options.ssl.all? do |k, v|
37
+ v == (k == :hostname ? uri.host : options.ssl[k])
38
+ end
39
+
40
+ @options.options_equals?(options, Options::REQUEST_BODY_IVARS + %i[@ssl])
41
+ end
42
+
43
+ def altsvc_match?(uri, other_uri)
44
+ other_uri = URI(other_uri)
45
+
46
+ uri.origin == other_uri.origin || begin
47
+ case uri.scheme
48
+ when "h2"
49
+ (other_uri.scheme == "https" || other_uri.scheme == "h2") &&
50
+ uri.host == other_uri.host &&
51
+ uri.port == other_uri.port
52
+ else
53
+ false
54
+ end
55
+ end
56
+ end
57
+ end
58
+
7
59
  @altsvc_mutex = Thread::Mutex.new
8
60
  @altsvcs = Hash.new { |h, k| h[k] = [] }
9
61
 
@@ -46,7 +98,7 @@ module HTTPX
46
98
 
47
99
  altsvc = response.headers["alt-svc"]
48
100
 
49
- # https://tools.ietf.org/html/rfc7838#section-3
101
+ # https://datatracker.ietf.org/doc/html/rfc7838#section-3
50
102
  # A field value containing the special value "clear" indicates that the
51
103
  # origin requests all alternatives for that origin to be invalidated
52
104
  # (including those specified in the same response, in case of an
@@ -99,7 +151,10 @@ module HTTPX
99
151
  end
100
152
 
101
153
  def parse_altsvc_origin(alt_proto, alt_origin)
102
- alt_scheme = parse_altsvc_scheme(alt_proto) or return
154
+ alt_scheme = parse_altsvc_scheme(alt_proto)
155
+
156
+ return unless alt_scheme
157
+
103
158
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
104
159
 
105
160
  URI.parse("#{alt_scheme}://#{alt_origin}")
@@ -10,19 +10,6 @@ module HTTPX
10
10
  MOD
11
11
  end
12
12
 
13
- %i[
14
- connection_opened connection_closed
15
- request_error
16
- request_started request_body_chunk request_completed
17
- response_started response_body_chunk response_completed
18
- ].each do |meth|
19
- class_eval(<<-MOD, __FILE__, __LINE__ + 1)
20
- def on_#{meth}(&blk) # def on_connection_opened(&blk)
21
- on(:#{meth}, &blk) # on(:connection_opened, &blk)
22
- end # end
23
- MOD
24
- end
25
-
26
13
  def request(*args, **options)
27
14
  branch(default_options).request(*args, **options)
28
15
  end
@@ -46,12 +33,6 @@ module HTTPX
46
33
  branch(default_options.merge(options), &blk)
47
34
  end
48
35
 
49
- protected
50
-
51
- def on(*args, &blk)
52
- branch(default_options).on(*args, &blk)
53
- end
54
-
55
36
  private
56
37
 
57
38
  def default_options
@@ -64,22 +45,52 @@ module HTTPX
64
45
  Session.new(options, &blk)
65
46
  end
66
47
 
67
- def method_missing(meth, *args, **options)
68
- return super unless meth =~ /\Awith_(.+)/
48
+ def method_missing(meth, *args, **options, &blk)
49
+ case meth
50
+ when /\Awith_(.+)/
69
51
 
70
- option = Regexp.last_match(1)
52
+ option = Regexp.last_match(1)
71
53
 
72
- return super unless option
54
+ return super unless option
73
55
 
74
- with(option.to_sym => (args.first || options))
75
- end
56
+ with(option.to_sym => args.first || options)
57
+ when /\Aon_(.+)/
58
+ callback = Regexp.last_match(1)
76
59
 
77
- def respond_to_missing?(meth, *)
78
- return super unless meth =~ /\Awith_(.+)/
60
+ return super unless %w[
61
+ connection_opened connection_closed
62
+ request_error
63
+ request_started request_body_chunk request_completed
64
+ response_started response_body_chunk response_completed
65
+ ].include?(callback)
79
66
 
80
- option = Regexp.last_match(1)
67
+ warn "DEPRECATION WARNING: calling `.#{meth}` on plain HTTPX sessions is deprecated. " \
68
+ "Use HTTPX.plugin(:callbacks).#{meth} instead."
81
69
 
82
- default_options.respond_to?(option) || super
70
+ plugin(:callbacks).__send__(meth, *args, **options, &blk)
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def respond_to_missing?(meth, *)
77
+ case meth
78
+ when /\Awith_(.+)/
79
+ option = Regexp.last_match(1)
80
+
81
+ default_options.respond_to?(option) || super
82
+ when /\Aon_(.+)/
83
+ callback = Regexp.last_match(1)
84
+
85
+ %w[
86
+ connection_opened connection_closed
87
+ request_error
88
+ request_started request_body_chunk request_completed
89
+ response_started response_body_chunk response_completed
90
+ ].include?(callback) || super
91
+ else
92
+ super
93
+ end
83
94
  end
84
95
  end
85
96
  end
@@ -12,6 +12,8 @@ module HTTPX
12
12
 
13
13
  attr_reader :pending, :requests
14
14
 
15
+ attr_accessor :max_concurrent_requests
16
+
15
17
  def initialize(buffer, options)
16
18
  @options = Options.new(options)
17
19
  @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
@@ -47,6 +49,7 @@ module HTTPX
47
49
  @max_requests = @options.max_requests || MAX_REQUESTS
48
50
  @parser.reset!
49
51
  @handshake_completed = false
52
+ @pending.concat(@requests) unless @requests.empty?
50
53
  end
51
54
 
52
55
  def close
@@ -218,6 +221,7 @@ module HTTPX
218
221
  end
219
222
 
220
223
  def ping
224
+ reset
221
225
  emit(:reset)
222
226
  emit(:exhausted)
223
227
  end
@@ -262,6 +266,7 @@ module HTTPX
262
266
 
263
267
  def disable
264
268
  disable_pipelining
269
+ reset
265
270
  emit(:reset)
266
271
  throw(:called)
267
272
  end
@@ -292,29 +297,31 @@ module HTTPX
292
297
  request.body.chunk!
293
298
  end
294
299
 
295
- connection = request.headers["connection"]
300
+ extra_headers = {}
296
301
 
297
- connection ||= if request.persistent?
298
- # when in a persistent connection, the request can't be at
299
- # the edge of a renegotiation
300
- if @requests.index(request) + 1 < @max_requests
301
- "keep-alive"
302
- else
303
- "close"
304
- end
305
- else
306
- # when it's not a persistent connection, it sets "Connection: close" always
307
- # on the last request of the possible batch (either allowed max requests,
308
- # or if smaller, the size of the batch itself)
309
- requests_limit = [@max_requests, @requests.size].min
310
- if request == @requests[requests_limit - 1]
311
- "close"
302
+ unless request.headers.key?("connection")
303
+ connection_value = if request.persistent?
304
+ # when in a persistent connection, the request can't be at
305
+ # the edge of a renegotiation
306
+ if @requests.index(request) + 1 < @max_requests
307
+ "keep-alive"
308
+ else
309
+ "close"
310
+ end
312
311
  else
313
- "keep-alive"
312
+ # when it's not a persistent connection, it sets "Connection: close" always
313
+ # on the last request of the possible batch (either allowed max requests,
314
+ # or if smaller, the size of the batch itself)
315
+ requests_limit = [@max_requests, @requests.size].min
316
+ if request == @requests[requests_limit - 1]
317
+ "close"
318
+ else
319
+ "keep-alive"
320
+ end
314
321
  end
315
- end
316
322
 
317
- extra_headers = { "connection" => connection }
323
+ extra_headers["connection"] = connection_value
324
+ end
318
325
  extra_headers["host"] = request.authority unless request.headers.key?("host")
319
326
  extra_headers
320
327
  end
@@ -370,12 +377,10 @@ module HTTPX
370
377
  end
371
378
 
372
379
  def join_headers2(headers)
373
- buffer = "".b
374
380
  headers.each do |field, value|
375
- buffer << "#{capitalized(field)}: #{value}" << CRLF
381
+ buffer = "#{capitalized(field)}: #{value}#{CRLF}"
376
382
  log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
377
383
  @buffer << buffer
378
- buffer.clear
379
384
  end
380
385
  end
381
386
 
@@ -55,7 +55,7 @@ module HTTPX
55
55
  return :w
56
56
  end
57
57
 
58
- unless (@connection.state == :connected && @handshake_completed)
58
+ unless @connection.state == :connected && @handshake_completed
59
59
  return @buffer.empty? ? :r : :rw
60
60
  end
61
61
 
@@ -73,8 +73,11 @@ module HTTPX
73
73
  end
74
74
 
75
75
  def close
76
- @connection.goaway unless @connection.state == :closed
77
- emit(:close)
76
+ unless @connection.state == :closed
77
+ @connection.goaway
78
+ emit(:timeout, @options.timeout[:close_handshake_timeout])
79
+ end
80
+ emit(:close, true)
78
81
  end
79
82
 
80
83
  def empty?
@@ -147,6 +150,7 @@ module HTTPX
147
150
 
148
151
  def send_pending
149
152
  while (request = @pending.shift)
153
+ # TODO: this request should go back to top of stack
150
154
  break unless send(request)
151
155
  end
152
156
  end
@@ -47,11 +47,11 @@ module HTTPX
47
47
 
48
48
  attr_accessor :family
49
49
 
50
- def initialize(type, uri, options)
51
- @type = type
50
+ def initialize(uri, options)
52
51
  @origins = [uri.origin]
53
52
  @origin = Utils.to_uri(uri.origin)
54
53
  @options = Options.new(options)
54
+ @type = initialize_type(uri, @options)
55
55
  @window_size = @options.window_size
56
56
  @read_buffer = Buffer.new(@options.buffer_size)
57
57
  @write_buffer = Buffer.new(@options.buffer_size)
@@ -92,18 +92,14 @@ module HTTPX
92
92
  def match?(uri, options)
93
93
  return false if !used? && (@state == :closing || @state == :closed)
94
94
 
95
- return false if exhausted?
96
-
97
95
  (
98
- (
99
- @origins.include?(uri.origin) &&
100
- # if there is more than one origin to match, it means that this connection
101
- # was the result of coalescing. To prevent blind trust in the case where the
102
- # origin came from an ORIGIN frame, we're going to verify the hostname with the
103
- # SSL certificate
104
- (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
105
- ) && @options == options
106
- ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
96
+ @origins.include?(uri.origin) &&
97
+ # if there is more than one origin to match, it means that this connection
98
+ # was the result of coalescing. To prevent blind trust in the case where the
99
+ # origin came from an ORIGIN frame, we're going to verify the hostname with the
100
+ # SSL certificate
101
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
102
+ ) && @options == options
107
103
  end
108
104
 
109
105
  def expired?
@@ -115,8 +111,6 @@ module HTTPX
115
111
  def mergeable?(connection)
116
112
  return false if @state == :closing || @state == :closed || !@io
117
113
 
118
- return false if exhausted?
119
-
120
114
  return false unless connection.addresses
121
115
 
122
116
  (
@@ -139,7 +133,7 @@ module HTTPX
139
133
  end
140
134
 
141
135
  def create_idle(options = {})
142
- self.class.new(@type, @origin, @options.merge(options))
136
+ self.class.new(@origin, @options.merge(options))
143
137
  end
144
138
 
145
139
  def merge(connection)
@@ -167,24 +161,6 @@ module HTTPX
167
161
  end
168
162
  end
169
163
 
170
- # checks if this is connection is an alternative service of
171
- # +uri+
172
- def match_altsvcs?(uri)
173
- @origins.any? { |origin| uri.altsvc_match?(origin) } ||
174
- AltSvc.cached_altsvc(@origin).any? do |altsvc|
175
- origin = altsvc["origin"]
176
- origin.altsvc_match?(uri.origin)
177
- end
178
- end
179
-
180
- def match_altsvc_options?(uri, options)
181
- return @options == options unless @options.ssl[:hostname] == uri.host
182
-
183
- dup_options = @options.merge(ssl: { hostname: nil })
184
- dup_options.ssl.delete(:hostname)
185
- dup_options == options
186
- end
187
-
188
164
  def connecting?
189
165
  @state == :idle
190
166
  end
@@ -223,7 +199,6 @@ module HTTPX
223
199
  when :closing
224
200
  consume
225
201
  transition(:closed)
226
- emit(:close)
227
202
  when :open
228
203
  consume
229
204
  end
@@ -236,24 +211,33 @@ module HTTPX
236
211
  @parser.close if @parser
237
212
  end
238
213
 
214
+ def terminate
215
+ @connected_at = nil if @state == :closed
216
+
217
+ close
218
+ end
219
+
239
220
  # bypasses the state machine to force closing of connections still connecting.
240
221
  # **only** used for Happy Eyeballs v2.
241
222
  def force_reset
242
223
  @state = :closing
243
224
  transition(:closed)
244
- emit(:close)
245
225
  end
246
226
 
247
227
  def reset
228
+ return if @state == :closing || @state == :closed
229
+
248
230
  transition(:closing)
231
+ unless @write_buffer.empty?
232
+ # handshakes, try sending
233
+ consume
234
+ @write_buffer.clear
235
+ end
249
236
  transition(:closed)
250
- emit(:close)
251
237
  end
252
238
 
253
239
  def send(request)
254
240
  if @parser && !@write_buffer.full?
255
- request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
256
-
257
241
  if @response_received_at && @keep_alive_timeout &&
258
242
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
259
243
  # when pushing a request into an existing connection, we have to check whether there
@@ -319,10 +303,6 @@ module HTTPX
319
303
  transition(:open)
320
304
  end
321
305
 
322
- def exhausted?
323
- @parser && parser.exhausted?
324
- end
325
-
326
306
  def consume
327
307
  return unless @io
328
308
 
@@ -497,34 +477,25 @@ module HTTPX
497
477
  request.emit(:promise, parser, stream)
498
478
  end
499
479
  parser.on(:exhausted) do
480
+ @pending.concat(parser.pending)
500
481
  emit(:exhausted)
501
482
  end
502
483
  parser.on(:origin) do |origin|
503
484
  @origins |= [origin]
504
485
  end
505
486
  parser.on(:close) do |force|
506
- if @state != :closed
507
- transition(:closing)
508
- if force || @state == :idle
509
- transition(:closed)
510
- emit(:close)
511
- end
487
+ if force
488
+ reset
489
+ emit(:terminate)
512
490
  end
513
491
  end
514
492
  parser.on(:close_handshake) do
515
493
  consume
516
494
  end
517
495
  parser.on(:reset) do
518
- if parser.empty?
519
- reset
520
- else
521
- transition(:closing)
522
- transition(:closed)
523
-
524
- @parser.reset if @parser
525
- transition(:idle)
526
- transition(:open)
527
- end
496
+ @pending.concat(parser.pending) unless parser.empty?
497
+ reset
498
+ idling unless @pending.empty?
528
499
  end
529
500
  parser.on(:current_timeout) do
530
501
  @current_timeout = @timeout = parser.timeout
@@ -593,13 +564,14 @@ module HTTPX
593
564
  when :inactive
594
565
  return unless @state == :open
595
566
  when :closing
596
- return unless @state == :open
567
+ return unless @state == :idle || @state == :open
597
568
 
598
569
  when :closed
599
570
  return unless @state == :closing
600
571
  return unless @write_buffer.empty?
601
572
 
602
573
  purge_after_closed
574
+ emit(:close) if @pending.empty?
603
575
  when :already_open
604
576
  nextstate = :open
605
577
  # the first check for given io readiness must still use a timeout.
@@ -621,6 +593,19 @@ module HTTPX
621
593
  @timeout = nil
622
594
  end
623
595
 
596
+ def initialize_type(uri, options)
597
+ options.transport || begin
598
+ case uri.scheme
599
+ when "http"
600
+ "tcp"
601
+ when "https"
602
+ "ssl"
603
+ else
604
+ raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
605
+ end
606
+ end
607
+ end
608
+
624
609
  def build_socket(addrs = nil)
625
610
  case @type
626
611
  when "tcp"
@@ -54,21 +54,6 @@ module HTTPX
54
54
  def origin
55
55
  "#{scheme}://#{authority}"
56
56
  end unless URI::HTTP.method_defined?(:origin)
57
-
58
- def altsvc_match?(uri)
59
- uri = URI.parse(uri)
60
-
61
- origin == uri.origin || begin
62
- case scheme
63
- when "h2"
64
- (uri.scheme == "https" || uri.scheme == "h2") &&
65
- host == uri.host &&
66
- (port || default_port) == (uri.port || uri.default_port)
67
- else
68
- false
69
- end
70
- end
71
- end
72
57
  end
73
58
  end
74
59
  end