httpx 0.7.0 → 0.10.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 (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