httpx 0.7.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_1_0.md +9 -0
  11. data/doc/release_notes/0_2_0.md +5 -0
  12. data/doc/release_notes/0_2_1.md +16 -0
  13. data/doc/release_notes/0_3_0.md +12 -0
  14. data/doc/release_notes/0_3_1.md +6 -0
  15. data/doc/release_notes/0_4_0.md +51 -0
  16. data/doc/release_notes/0_4_1.md +3 -0
  17. data/doc/release_notes/0_5_0.md +15 -0
  18. data/doc/release_notes/0_5_1.md +14 -0
  19. data/doc/release_notes/0_6_0.md +5 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_6_2.md +6 -0
  22. data/doc/release_notes/0_6_3.md +13 -0
  23. data/doc/release_notes/0_6_4.md +21 -0
  24. data/doc/release_notes/0_6_5.md +22 -0
  25. data/doc/release_notes/0_6_6.md +19 -0
  26. data/doc/release_notes/0_6_7.md +5 -0
  27. data/doc/release_notes/0_7_0.md +46 -0
  28. data/doc/release_notes/0_8_0.md +27 -0
  29. data/doc/release_notes/0_8_1.md +8 -0
  30. data/doc/release_notes/0_8_2.md +7 -0
  31. data/doc/release_notes/0_9_0.md +38 -0
  32. data/lib/httpx.rb +2 -0
  33. data/lib/httpx/adapters/faraday.rb +1 -1
  34. data/lib/httpx/altsvc.rb +18 -2
  35. data/lib/httpx/chainable.rb +9 -8
  36. data/lib/httpx/connection.rb +177 -72
  37. data/lib/httpx/connection/http1.rb +44 -13
  38. data/lib/httpx/connection/http2.rb +77 -34
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +1 -0
  41. data/lib/httpx/extensions.rb +23 -3
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +11 -4
  44. data/lib/httpx/io/tcp.rb +16 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/loggable.rb +6 -6
  47. data/lib/httpx/options.rb +22 -15
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +49 -64
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +45 -17
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  58. data/lib/httpx/plugins/expect.rb +12 -1
  59. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  60. data/lib/httpx/plugins/h2c.rb +1 -1
  61. data/lib/httpx/plugins/multipart.rb +0 -8
  62. data/lib/httpx/plugins/persistent.rb +6 -1
  63. data/lib/httpx/plugins/proxy.rb +16 -12
  64. data/lib/httpx/plugins/proxy/http.rb +7 -2
  65. data/lib/httpx/plugins/proxy/socks4.rb +4 -2
  66. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  67. data/lib/httpx/plugins/push_promise.rb +2 -2
  68. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  69. data/lib/httpx/plugins/retries.rb +13 -6
  70. data/lib/httpx/plugins/stream.rb +109 -13
  71. data/lib/httpx/pool.rb +13 -15
  72. data/lib/httpx/registry.rb +2 -1
  73. data/lib/httpx/request.rb +14 -19
  74. data/lib/httpx/resolver.rb +7 -8
  75. data/lib/httpx/resolver/https.rb +22 -5
  76. data/lib/httpx/resolver/native.rb +27 -33
  77. data/lib/httpx/resolver/options.rb +2 -2
  78. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  79. data/lib/httpx/response.rb +22 -17
  80. data/lib/httpx/selector.rb +96 -97
  81. data/lib/httpx/session.rb +32 -24
  82. data/lib/httpx/timeout.rb +7 -1
  83. data/lib/httpx/transcoder/chunker.rb +0 -2
  84. data/lib/httpx/transcoder/form.rb +0 -6
  85. data/lib/httpx/transcoder/json.rb +0 -4
  86. data/lib/httpx/utils.rb +45 -0
  87. data/lib/httpx/version.rb +1 -1
  88. data/sig/buffer.rbs +24 -0
  89. data/sig/callbacks.rbs +14 -0
  90. data/sig/chainable.rbs +37 -0
  91. data/sig/connection.rbs +2 -0
  92. data/sig/connection/http2.rbs +4 -0
  93. data/sig/domain_name.rbs +17 -0
  94. data/sig/errors.rbs +3 -0
  95. data/sig/headers.rbs +42 -0
  96. data/sig/httpx.rbs +14 -0
  97. data/sig/loggable.rbs +11 -0
  98. data/sig/missing.rbs +12 -0
  99. data/sig/options.rbs +118 -0
  100. data/sig/parser/http1.rbs +50 -0
  101. data/sig/plugins/authentication.rbs +11 -0
  102. data/sig/plugins/basic_authentication.rbs +13 -0
  103. data/sig/plugins/compression.rbs +55 -0
  104. data/sig/plugins/compression/brotli.rbs +21 -0
  105. data/sig/plugins/compression/deflate.rbs +17 -0
  106. data/sig/plugins/compression/gzip.rbs +29 -0
  107. data/sig/plugins/cookies.rbs +26 -0
  108. data/sig/plugins/cookies/cookie.rbs +50 -0
  109. data/sig/plugins/cookies/jar.rbs +27 -0
  110. data/sig/plugins/digest_authentication.rbs +33 -0
  111. data/sig/plugins/expect.rbs +19 -0
  112. data/sig/plugins/follow_redirects.rbs +37 -0
  113. data/sig/plugins/h2c.rbs +26 -0
  114. data/sig/plugins/multipart.rbs +19 -0
  115. data/sig/plugins/persistent.rbs +17 -0
  116. data/sig/plugins/proxy.rbs +47 -0
  117. data/sig/plugins/proxy/http.rbs +14 -0
  118. data/sig/plugins/proxy/socks4.rbs +33 -0
  119. data/sig/plugins/proxy/socks5.rbs +36 -0
  120. data/sig/plugins/proxy/ssh.rbs +18 -0
  121. data/sig/plugins/push_promise.rbs +22 -0
  122. data/sig/plugins/rate_limiter.rbs +11 -0
  123. data/sig/plugins/retries.rbs +48 -0
  124. data/sig/plugins/stream.rbs +39 -0
  125. data/sig/pool.rbs +2 -0
  126. data/sig/registry.rbs +9 -0
  127. data/sig/request.rbs +61 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/session.rbs +49 -0
  130. data/sig/test.rbs +9 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +16 -0
  133. data/sig/transcoder/body.rbs +18 -0
  134. data/sig/transcoder/chunker.rbs +32 -0
  135. data/sig/transcoder/form.rbs +16 -0
  136. data/sig/transcoder/json.rbs +14 -0
  137. metadata +120 -21
@@ -0,0 +1,19 @@
1
+ # 0.6.6
2
+
3
+ ## Features
4
+
5
+ * The `retries` plugin receives two new options:
6
+ * `retry_on`: a callable that receives the failed response as an argument; the return value will determine whether there'll be a retried request.
7
+ * `retry_after`: time (in seconds) after which there request will be retried. Can be an integer or a callable that receives the request and returns an integer (one can do exponential back-off like that, for example).
8
+ * Added support for DNS-over-HTTPS GET requests as per the latest spec.
9
+
10
+ ## Improvements
11
+
12
+ * `HTTPX.plugins` got deprecated; basically, it's great until you have to pass options to a plugin, and then it just works (not). The recommended way to load multiple plugins is `HTTPX.plugin(...).plugin(...)`.
13
+
14
+
15
+ ## Bugfixes
16
+
17
+ * fixed a proxy bug where an `Alt-Svc` response header would make the client try to connect. Just like connection coalescing and the ORIGIN frame, it ignores it when going through a proxy.
18
+
19
+
@@ -0,0 +1,5 @@
1
+ # 0.6.7
2
+
3
+ ## Bugfixes
4
+
5
+ * An error was reported when using the follow plugin allowing insecure redirects: if the insecure redirect would be for the same host, and the original request was performed with HTTP/2, the library would try to coalesce the request, and blocks the reactor. A check was made to ensure that connection only coalesces if both are https connections.
@@ -0,0 +1,46 @@
1
+ # 0.7.0
2
+
3
+
4
+ ## Features
5
+
6
+ New option: `:max_requests`. This is a connection-level option signalizing how many requests can be performed on a connection. Although the HTTP/1 parser defined this well, in HTTP/2 this wasn't very clear, so: by definition, the remote MAX_CONCURRENT_STREAMS setting will be used to define it, unless the user explicitly passed the option. You can also pass `:max_requests => Float::INFINITY` if you know that the server allows more requests than that on a connection.
7
+
8
+ New plugin: `:expect`.
9
+
10
+ Although there was support for `expect: 100-continue` header already when passed, this plugin can:
11
+
12
+ * automatically set the header on requests with body;
13
+ * execute the flow;
14
+ * recover from 417 status errors (i.e. try again without it);
15
+ * send body after X seconds if no 100 response came;
16
+
17
+ Suport for `with_` methods for the session. As long as the suffix is a valid attribute, it's just like that:
18
+
19
+ ```ruby
20
+ HTTPX.with_timeout(...).with_ssl(...)
21
+ # same as:
22
+ # HTTPX.with(timeout: ..., ssl: ...)
23
+ ```
24
+
25
+ ## Improvements
26
+
27
+ ### Connections
28
+
29
+ The following improvements make the `persistent` plugin way more resilient:
30
+
31
+ * Better balancing of HTTP/2 connections by distributing requests among X connections depending of how many requests they can process.
32
+ * Exhausted connections can off-load to a new same-origin connection (such as, when the server negotiates less `MAX_CONCURRENT_STREAMS` than what's expected).
33
+
34
+ ### Timeouts
35
+
36
+ (Timeouts will be one of the main improvements from the 0.7.x series)
37
+
38
+ `:total_timeout` is now a connection-level directive, which means that this feature will actually make more sense and account for all requests in a block at the same time, instead of one-by-one.
39
+
40
+ ### Options
41
+
42
+ Option setters were being bypassed, therefore a lot of the type-checks defined there weren't effectively being picked upon, which could have led to weird user errors.
43
+
44
+ ## Bugfixes
45
+
46
+ * fixed the `push_promise` plugin integration (wasn't working well since `http-2-next` was adopted);
@@ -0,0 +1,27 @@
1
+ # 0.8.0
2
+
3
+
4
+ ## Features
5
+
6
+ * `keep_alive_timeout`: for persistent connections, the keep alive timeout will set the connection to be closed if not reused for a request **after** the last received response;
7
+
8
+ ## Improvements
9
+
10
+ * using `max_requests` for HTTP/1 pipelining as well;
11
+ * `retries` plugin now works with plain HTTP responses (not just error responses);
12
+ * reduced the number of string allocations from log labels;
13
+ * performance: a lot of improvements were made to optimize the "waiting for IO events" phase, which dramatically reduced the CPU usage and make the performance of the library more on-par with other ruby HTTP gems for the 1-shot request scenario.
14
+
15
+
16
+ ## Bugfixes
17
+
18
+ * fixed `HTTPX::Response#copy_to`;
19
+ * fixed `compression` plugin not properly compressing request bodies using `gzip`;
20
+ * fixed `compression` plugin not handling `content-encoding: identity` payloads;
21
+ * do not overwrite user-defined `max_requests`on HTTP2 connection handshake;
22
+ * `retries` plugin: connection was blocking when a request with body was retried;
23
+ * `alt-svc: clear` response header was causing the process to hang;
24
+
25
+ ## Tests
26
+
27
+ * Code coverage improved to 91%;
@@ -0,0 +1,8 @@
1
+ # 0.8.1
2
+
3
+
4
+ ## Bugfixes
5
+
6
+ * fixing HTTP/2 handshake IO interests calculation;
7
+ * fixed the double ctrl+f issue when terminating an ongoing HTTP/2 request;
8
+ * fixed connection comparison when passing headers;
@@ -0,0 +1,7 @@
1
+ # 0.8.2
2
+
3
+ ## Features
4
+
5
+ * `:expect` plugin now supports a new option, `:expect_threshold_size`, meaning: the byte size threshold below which no `expect` header will be sent with requests with payload.
6
+ * `:compression` plugin now supports a new option, `:compression_threshold_size`, meaning: the bytesize threshold below which request payload won't be compressed before being sent.
7
+ * for HTTP/2 connections, when `keep_alive_timeout` expires, a `PING` frame is used to check connection availability; if successful, the connection will be reused.
@@ -0,0 +1,38 @@
1
+ # 0.9.0
2
+
3
+ ## Features
4
+
5
+ ### Multiple requests with specific options
6
+
7
+ You can now pass a third element to the "request element" of an array to `.request`.
8
+
9
+ ```ruby
10
+ requests = [
11
+ [:post, "https://url/post", { form: { foo: "bar" } }],
12
+ [:post, "https://url/post", { form: { foo: "bar2" } }]
13
+ ]
14
+ HTTPX.request(requests)
15
+ # or, if you want to pass options common to all requests
16
+ HTTPX.request(requests, max_concurrent_requests: 1)
17
+ ```
18
+
19
+
20
+ ### HTTPX::Session#build_request
21
+
22
+ `HTTPX::Session::build_request` is now public API from a session. You can now build requests before you send them. These request objects are still considered somewhat "internal", so consider them immutable and **do not rely on its API**. Just pass them forward.
23
+
24
+ Note: this API is only available for instantiated session, so there is no `HTTPX.build_request`.
25
+
26
+
27
+ ```ruby
28
+
29
+ HTTPX.wrap do |http|
30
+ requests = [
31
+ http.build_request(:post, "https://url/post", { form: { foo: "bar" } }),
32
+ http.build_request(:post, "https://url/post", { form: { foo: "bar2" } })
33
+ ]
34
+ http.request(requests)
35
+ # or, if you want to pass options common to all requests
36
+ http.request(requests, max_concurrent_requests: 1)
37
+ end
38
+ ```
@@ -5,6 +5,8 @@ require "httpx/version"
5
5
  require "httpx/extensions"
6
6
 
7
7
  require "httpx/errors"
8
+ require "httpx/utils"
9
+ require "httpx/domain_name"
8
10
  require "httpx/altsvc"
9
11
  require "httpx/callbacks"
10
12
  require "httpx/loggable"
@@ -121,7 +121,7 @@ module Faraday
121
121
  end
122
122
 
123
123
  def respond_to_missing?(meth)
124
- @env.respond_to?(meth)
124
+ @env.respond_to?(meth) || super
125
125
  end
126
126
 
127
127
  def method_missing(meth, *args, &blk)
@@ -42,7 +42,23 @@ module HTTPX
42
42
 
43
43
  origin = request.origin
44
44
  host = request.uri.host
45
- parse(response.headers["alt-svc"]) do |alt_origin, alt_params|
45
+
46
+ altsvc = response.headers["alt-svc"]
47
+
48
+ # https://tools.ietf.org/html/rfc7838#section-3
49
+ # A field value containing the special value "clear" indicates that the
50
+ # origin requests all alternatives for that origin to be invalidated
51
+ # (including those specified in the same response, in case of an
52
+ # invalid reply containing both "clear" and alternative services).
53
+ if altsvc == "clear"
54
+ @altsvc_mutex.synchronize do
55
+ @altsvcs[origin].clear
56
+ end
57
+
58
+ return
59
+ end
60
+
61
+ parse(altsvc) do |alt_origin, alt_params|
46
62
  alt_origin.host ||= host
47
63
  yield(alt_origin, origin, alt_params)
48
64
  end
@@ -73,7 +89,7 @@ module HTTPX
73
89
  alt_proto, alt_origin = alt_origin.split("=")
74
90
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
75
91
  if alt_origin.start_with?(":")
76
- alt_origin = "dummy#{alt_origin}"
92
+ alt_origin = "#{alt_proto}://dummy#{alt_origin}"
77
93
  uri = URI.parse(alt_origin)
78
94
  uri.host = nil
79
95
  uri
@@ -3,13 +3,15 @@
3
3
  module HTTPX
4
4
  module Chainable
5
5
  %i[head get post put delete trace options connect patch].each do |meth|
6
- define_method meth do |*uri, **options|
7
- request(meth, uri, **options)
8
- end
6
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
7
+ def #{meth}(*uri, **options)
8
+ request(:#{meth}, uri, **options)
9
+ end
10
+ MOD
9
11
  end
10
12
 
11
- def request(verb, uri, **options)
12
- branch(default_options).request(verb, uri, **options)
13
+ def request(*args, **options)
14
+ branch(default_options).request(*args, **options)
13
15
  end
14
16
 
15
17
  # :nocov:
@@ -32,11 +34,11 @@ module HTTPX
32
34
  branch(default_options).wrap(&blk)
33
35
  end
34
36
 
35
- def plugin(*args, **opts)
37
+ def plugin(*args, **opts, &blk)
36
38
  klass = is_a?(Session) ? self.class : Session
37
39
  klass = Class.new(klass)
38
40
  klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
39
- klass.plugin(*args, **opts).new
41
+ klass.plugin(*args, **opts, &blk).new
40
42
  end
41
43
 
42
44
  # deprecated
@@ -57,7 +59,6 @@ module HTTPX
57
59
  @options || Options.new
58
60
  end
59
61
 
60
- # :nodoc:
61
62
  def branch(options, &blk)
62
63
  return self.class.new(options, &blk) if is_a?(Session)
63
64
 
@@ -51,7 +51,7 @@ module HTTPX
51
51
  def initialize(type, uri, options)
52
52
  @type = type
53
53
  @origins = [uri.origin]
54
- @origin = URI(uri.origin)
54
+ @origin = Utils.uri(uri.origin)
55
55
  @options = Options.new(options)
56
56
  @window_size = @options.window_size
57
57
  @read_buffer = Buffer.new(BUFFER_SIZE)
@@ -67,6 +67,10 @@ module HTTPX
67
67
  else
68
68
  transition(:idle)
69
69
  end
70
+
71
+ @inflight = 0
72
+ @keep_alive_timeout = options.timeout.keep_alive_timeout
73
+ @keep_alive_timer = nil
70
74
  end
71
75
 
72
76
  # this is a semi-private method, to be used by the resolver
@@ -138,14 +142,15 @@ module HTTPX
138
142
  end
139
143
  end
140
144
 
141
- def purge_pending
145
+ def purge_pending(&block)
142
146
  pendings = []
143
- pendings << @parser.pending if @parser
147
+ if @parser
148
+ @inflight -= @parser.pending.size
149
+ pendings << @parser.pending
150
+ end
144
151
  pendings << @pending
145
152
  pendings.each do |pending|
146
- pending.reject! do |request|
147
- yield request
148
- end
153
+ pending.reject!(&block)
149
154
  end
150
155
  end
151
156
 
@@ -168,22 +173,45 @@ module HTTPX
168
173
  end
169
174
 
170
175
  def interests
171
- return :w if @state == :idle
176
+ # connecting
177
+ if connecting?
178
+ connect
179
+
180
+ return @io.interests if connecting?
181
+ end
172
182
 
173
- :rw
183
+ # if the write buffer is full, we drain it
184
+ return :w if @write_buffer.full?
185
+
186
+ return @parser.interests if @parser
187
+
188
+ nil
174
189
  end
175
190
 
176
191
  def to_io
192
+ @io.to_io
193
+ end
194
+
195
+ def call
177
196
  case @state
178
- when :idle
179
- transition(:open)
197
+ when :closed
198
+ return
199
+ when :closing
200
+ consume
201
+ transition(:closed)
202
+ emit(:close)
203
+ when :open
204
+ consume
180
205
  end
181
- @io.to_io
206
+ nil
182
207
  end
183
208
 
184
209
  def close
185
210
  @parser.close if @parser
186
- transition(:closing)
211
+ return unless @keep_alive_timer
212
+
213
+ @keep_alive_timer.cancel
214
+ remove_instance_variable(:@keep_alive_timer)
187
215
  end
188
216
 
189
217
  def reset
@@ -195,26 +223,25 @@ module HTTPX
195
223
  def send(request)
196
224
  if @parser && !@write_buffer.full?
197
225
  request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
226
+ if @keep_alive_timer
227
+ # when pushing a request into an existing connection, we have to check whether there
228
+ # is the possibility that the connection might have extended the keep alive timeout.
229
+ # for such cases, we want to ping for availability before deciding to shovel requests.
230
+ if @keep_alive_timer.fires_in.negative?
231
+ @pending << request
232
+ parser.ping
233
+ return
234
+ end
235
+
236
+ @keep_alive_timer.pause
237
+ end
238
+ @inflight += 1
198
239
  parser.send(request)
199
240
  else
200
241
  @pending << request
201
242
  end
202
243
  end
203
244
 
204
- def call
205
- case @state
206
- when :closed
207
- return
208
- when :closing
209
- dwrite
210
- transition(:closed)
211
- emit(:close)
212
- when :open
213
- consume
214
- end
215
- nil
216
- end
217
-
218
245
  def timeout
219
246
  return @timeout if defined?(@timeout)
220
247
 
@@ -225,54 +252,91 @@ module HTTPX
225
252
 
226
253
  private
227
254
 
255
+ def connect
256
+ transition(:open)
257
+ end
258
+
228
259
  def exhausted?
229
260
  @parser && parser.exhausted?
230
261
  end
231
262
 
232
263
  def consume
233
264
  catch(:called) do
234
- dread
235
- dwrite
236
- parser.consume
237
- end
238
- end
239
-
240
- def dread(wsize = @window_size)
241
- loop do
242
- siz = @io.read(wsize, @read_buffer)
243
- unless siz
244
- ex = EOFError.new("descriptor closed")
245
- ex.set_backtrace(caller)
246
- on_error(ex)
247
- return
248
- end
249
- return if siz.zero?
250
-
251
- log { "READ: #{siz} bytes..." }
252
- parser << @read_buffer.to_s
253
- return if @state == :closing || @state == :closed
254
- end
255
- end
256
-
257
- def dwrite
258
- loop do
259
- return if @write_buffer.empty?
260
-
261
- siz = @io.write(@write_buffer)
262
- unless siz
263
- ex = EOFError.new("descriptor closed")
264
- ex.set_backtrace(caller)
265
- on_error(ex)
266
- return
265
+ loop do
266
+ parser.consume
267
+
268
+ # we exit if there's no more data to process
269
+ if @pending.size.zero? && @inflight.zero?
270
+ log(level: 3) { "NO MORE REQUESTS..." }
271
+ return
272
+ end
273
+
274
+ @timeout = @current_timeout
275
+
276
+ read_drained = false
277
+ write_drained = nil
278
+
279
+ # dread
280
+ loop do
281
+ siz = @io.read(@window_size, @read_buffer)
282
+ unless siz
283
+ ex = EOFError.new("descriptor closed")
284
+ ex.set_backtrace(caller)
285
+ on_error(ex)
286
+ return
287
+ end
288
+
289
+ if siz.zero?
290
+ read_drained = @read_buffer.empty?
291
+ break
292
+ end
293
+
294
+ parser << @read_buffer.to_s
295
+
296
+ break if @state == :closing || @state == :closed
297
+
298
+ # for HTTP/2, we just want to write goaway frame
299
+ end unless @state == :closing
300
+
301
+ # dwrite
302
+ loop do
303
+ if @write_buffer.empty?
304
+ # we only mark as drained on the first loop
305
+ write_drained = write_drained.nil? && @inflight.positive?
306
+ break
307
+ end
308
+
309
+ siz = @io.write(@write_buffer)
310
+ unless siz
311
+ ex = EOFError.new("descriptor closed")
312
+ ex.set_backtrace(caller)
313
+ on_error(ex)
314
+ return
315
+ end
316
+
317
+ if siz.zero?
318
+ write_drained = !@write_buffer.empty?
319
+ break
320
+ end
321
+
322
+ break if @state == :closing || @state == :closed
323
+
324
+ write_drained = false
325
+ end
326
+
327
+ # return if socket is drained
328
+ if read_drained && write_drained
329
+ log(level: 3) { "WAITING FOR EVENTS..." }
330
+ return
331
+ end
267
332
  end
268
- log { "WRITE: #{siz} bytes..." }
269
- return if siz.zero?
270
- return if @state == :closing || @state == :closed
271
333
  end
272
334
  end
273
335
 
274
336
  def send_pending
275
337
  while !@write_buffer.full? && (request = @pending.shift)
338
+ @inflight += 1
339
+ @keep_alive_timer.pause if @keep_alive_timer
276
340
  parser.send(request)
277
341
  end
278
342
  end
@@ -292,12 +356,15 @@ module HTTPX
292
356
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
293
357
  emit(:altsvc, alt_origin, origin, alt_params)
294
358
  end
359
+ handle_response
295
360
  request.emit(:response, response)
296
361
  end
297
362
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
298
363
  emit(:altsvc, alt_origin, origin, alt_params)
299
364
  end
300
365
 
366
+ parser.on(:pong, &method(:send_pending))
367
+
301
368
  parser.on(:promise) do |request, stream|
302
369
  request.emit(:promise, parser, stream)
303
370
  end
@@ -307,14 +374,21 @@ module HTTPX
307
374
  parser.on(:origin) do |origin|
308
375
  @origins << origin
309
376
  end
310
- parser.on(:close) do
377
+ parser.on(:close) do |force|
311
378
  transition(:closing)
379
+ if force
380
+ transition(:closed)
381
+ emit(:close)
382
+ end
312
383
  end
313
384
  parser.on(:reset) do
314
- transition(:closing)
315
- unless parser.empty?
385
+ if parser.empty?
386
+ reset
387
+ else
388
+ transition(:closing)
316
389
  transition(:closed)
317
390
  emit(:reset)
391
+ @parser.reset if @parser
318
392
  transition(:idle)
319
393
  transition(:open)
320
394
  end
@@ -335,6 +409,9 @@ module HTTPX
335
409
 
336
410
  def transition(nextstate)
337
411
  case nextstate
412
+ when :idle
413
+ @timeout = @current_timeout = @options.timeout.connect_timeout
414
+
338
415
  when :open
339
416
  return if @state == :closed
340
417
 
@@ -344,6 +421,8 @@ module HTTPX
344
421
  return unless @io.connected?
345
422
 
346
423
  send_pending
424
+
425
+ @timeout = @current_timeout = @options.timeout.operation_timeout
347
426
  emit(:open)
348
427
  when :closing
349
428
  return unless @state == :open
@@ -358,6 +437,11 @@ module HTTPX
358
437
 
359
438
  @io.close
360
439
  @read_buffer.clear
440
+ if @keep_alive_timer
441
+ @keep_alive_timer.cancel
442
+ remove_instance_variable(:@keep_alive_timer)
443
+ end
444
+
361
445
  remove_instance_variable(:@timeout) if defined?(@timeout)
362
446
  when :already_open
363
447
  nextstate = :open
@@ -371,7 +455,6 @@ module HTTPX
371
455
  throw(:jump_tick)
372
456
  rescue Errno::ECONNREFUSED,
373
457
  Errno::EADDRNOTAVAIL,
374
- Errno::EHOSTUNREACH,
375
458
  OpenSSL::SSL::SSLError => e
376
459
  # connect errors, exit gracefully
377
460
  handle_error(e)
@@ -379,21 +462,43 @@ module HTTPX
379
462
  emit(:close)
380
463
  end
381
464
 
382
- def on_error(ex)
383
- handle_error(ex)
384
- reset
465
+ def handle_response
466
+ @inflight -= 1
467
+ return unless @inflight.zero?
468
+
469
+ if @keep_alive_timer
470
+ @keep_alive_timer.resume
471
+ @keep_alive_timer.reset
472
+ else
473
+ @keep_alive_timer = @timers.after(@keep_alive_timeout) do
474
+ unless @inflight.zero?
475
+ log { "(#{@origin}): keep alive timeout expired" }
476
+ parser.ping
477
+ end
478
+ end
479
+ end
385
480
  end
386
481
 
387
- def handle_error(error)
482
+ def on_error(error)
388
483
  if error.instance_of?(TimeoutError)
389
484
  if @timeout
390
485
  @timeout -= error.timeout
391
486
  return unless @timeout <= 0
392
487
  end
393
488
 
394
- error = error.to_connection_error if connecting?
489
+ if @total_timeout && @total_timeout.fires_in.negative?
490
+ ex = TotalTimeoutError.new(@total_timeout.interval, "Timed out after #{@total_timeout.interval} seconds")
491
+ ex.set_backtrace(error.backtrace)
492
+ error = ex
493
+ elsif connecting?
494
+ error = error.to_connection_error
495
+ end
395
496
  end
497
+ handle_error(error)
498
+ reset
499
+ end
396
500
 
501
+ def handle_error(error)
397
502
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
398
503
  while (request = @pending.shift)
399
504
  request.emit(:response, ErrorResponse.new(request, error, @options))
@@ -408,8 +513,8 @@ module HTTPX
408
513
  @total_timeout ||= @timers.after(total) do
409
514
  ex = TotalTimeoutError.new(total, "Timed out after #{total} seconds")
410
515
  ex.set_backtrace(caller)
411
- @parser.close if @parser
412
516
  on_error(ex)
517
+ @parser.close if @parser
413
518
  end
414
519
  end
415
520
  end