httpx 0.11.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_11_1.md +5 -1
  4. data/doc/release_notes/0_11_2.md +5 -0
  5. data/doc/release_notes/0_11_3.md +5 -0
  6. data/doc/release_notes/0_12_0.md +55 -0
  7. data/doc/release_notes/0_13_0.md +58 -0
  8. data/doc/release_notes/0_13_1.md +5 -0
  9. data/lib/httpx.rb +2 -1
  10. data/lib/httpx/adapters/faraday.rb +4 -6
  11. data/lib/httpx/altsvc.rb +1 -0
  12. data/lib/httpx/chainable.rb +2 -2
  13. data/lib/httpx/connection.rb +80 -28
  14. data/lib/httpx/connection/http1.rb +19 -6
  15. data/lib/httpx/connection/http2.rb +32 -25
  16. data/lib/httpx/io.rb +16 -3
  17. data/lib/httpx/io/ssl.rb +35 -24
  18. data/lib/httpx/io/tcp.rb +50 -28
  19. data/lib/httpx/io/tls.rb +218 -0
  20. data/lib/httpx/io/tls/box.rb +365 -0
  21. data/lib/httpx/io/tls/context.rb +199 -0
  22. data/lib/httpx/io/tls/ffi.rb +390 -0
  23. data/lib/httpx/io/unix.rb +27 -12
  24. data/lib/httpx/options.rb +11 -23
  25. data/lib/httpx/parser/http1.rb +4 -4
  26. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  27. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  28. data/lib/httpx/plugins/compression.rb +20 -8
  29. data/lib/httpx/plugins/compression/brotli.rb +8 -6
  30. data/lib/httpx/plugins/compression/deflate.rb +4 -7
  31. data/lib/httpx/plugins/compression/gzip.rb +2 -2
  32. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  33. data/lib/httpx/plugins/digest_authentication.rb +1 -1
  34. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  35. data/lib/httpx/plugins/h2c.rb +43 -58
  36. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  37. data/lib/httpx/plugins/multipart.rb +2 -0
  38. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  39. data/lib/httpx/plugins/proxy.rb +1 -1
  40. data/lib/httpx/plugins/proxy/http.rb +1 -1
  41. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  42. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  43. data/lib/httpx/plugins/push_promise.rb +3 -2
  44. data/lib/httpx/plugins/retries.rb +2 -2
  45. data/lib/httpx/plugins/stream.rb +6 -6
  46. data/lib/httpx/plugins/upgrade.rb +83 -0
  47. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  48. data/lib/httpx/pool.rb +14 -6
  49. data/lib/httpx/registry.rb +1 -7
  50. data/lib/httpx/request.rb +11 -1
  51. data/lib/httpx/resolver/https.rb +3 -11
  52. data/lib/httpx/response.rb +14 -7
  53. data/lib/httpx/selector.rb +5 -0
  54. data/lib/httpx/session.rb +25 -2
  55. data/lib/httpx/transcoder/body.rb +3 -5
  56. data/lib/httpx/version.rb +1 -1
  57. data/sig/chainable.rbs +2 -1
  58. data/sig/connection/http1.rbs +3 -2
  59. data/sig/connection/http2.rbs +5 -3
  60. data/sig/options.rbs +7 -20
  61. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  62. data/sig/plugins/aws_sigv4.rbs +64 -0
  63. data/sig/plugins/compression.rbs +5 -3
  64. data/sig/plugins/compression/brotli.rbs +1 -1
  65. data/sig/plugins/compression/deflate.rbs +1 -1
  66. data/sig/plugins/compression/gzip.rbs +1 -1
  67. data/sig/plugins/cookies.rbs +0 -1
  68. data/sig/plugins/digest_authentication.rbs +0 -1
  69. data/sig/plugins/expect.rbs +0 -2
  70. data/sig/plugins/follow_redirects.rbs +0 -2
  71. data/sig/plugins/h2c.rbs +5 -10
  72. data/sig/plugins/persistent.rbs +0 -1
  73. data/sig/plugins/proxy.rbs +0 -1
  74. data/sig/plugins/push_promise.rbs +1 -1
  75. data/sig/plugins/retries.rbs +0 -4
  76. data/sig/plugins/upgrade.rbs +23 -0
  77. data/sig/response.rbs +3 -1
  78. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b21be17b0c6a6d3e2f60a97582b88f33dc1f62e09e5555cabfc2d6bea1f06a7
4
- data.tar.gz: e0ca927150864a80a8050cc9fbc2c23e58b55452b536ce4301eceecebf1c6d37
3
+ metadata.gz: e3859c516c1cdddc64f8105fafd5425da26074582b1590cc87f65401a909e0e2
4
+ data.tar.gz: 209b5c1fba921d69f9aa2167ccb0f4b6b45a1a28667364eefef8026104004f53
5
5
  SHA512:
6
- metadata.gz: 1493d50a5b49dd7c83f48078d582f874cc204d3e5df3442d7ea8308b31e6fd011e30cc3fdc5f1825d0daebd270c22af387700ab8f6d2fb9183d2e1fb63bd7265
7
- data.tar.gz: ac48ea08cacc65a4948fea8a2abdd50fe7d7fd30c6260400cd61197ea7e6e8d87de2b4750ca1452f242569bbf6dc304c14865f809e7871a4f55d33e7e5543c03
6
+ metadata.gz: 3862b4879291aa944b92c50d232abc6413a170f2bcd501d6f33a1ca55a99a1411dd0d4f2b6a50b92a8b1465fa42b654ef658c15a91154205bfd60bcdc983c5a5
7
+ data.tar.gz: 6b2f721cc2bb8e0fe7267517dcd146fe50f448676bcec64e9744d5f358232d8d9c364acd6717e0df3a72bfcb1d154a7e4b74252324cf9a248f1fe062242eb4fe
data/README.md CHANGED
@@ -113,7 +113,7 @@ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/h
113
113
 
114
114
  ## Supported Rubies
115
115
 
116
- All Rubies greater or equal to 2.1, and always latest JRuby.
116
+ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
117
117
 
118
118
  **Note**: This gem is tested against all latest patch versions, i.e. if you're using 2.2.0 and you experience some issue, please test it against 2.2.10 (latest patch version of 2.2) before creating an issue.
119
119
 
@@ -133,7 +133,7 @@ All Rubies greater or equal to 2.1, and always latest JRuby.
133
133
 
134
134
  If your requirement is to run requests over HTTP/2 and TLS, make sure you run a version of the gem which compiles OpenSSL 1.0.2 (Ruby 2.3 and higher are guaranteed to).
135
135
 
136
- JRuby's `openssl` is based on Bouncy Castle, which is massively outdated and still doesn't implement ALPN. So HTTP/2 over TLS/ALPN negotiation is off until JRuby figures this out.
136
+ In order to use HTTP/2 under JRuby, [check this link](https://gitlab.com/honeyryderchuck/httpx/-/wikis/JRuby-Truffleruby-Other-Rubies) to know what to do.
137
137
 
138
138
  ### Known bugs
139
139
 
@@ -1 +1,5 @@
1
- 0_11_1.md
1
+ # 0.11.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixed a bug related to the `:compression` plugin trying to process a last empty DATA frame from an HTTP/2 response, after it had been closed.
@@ -0,0 +1,5 @@
1
+ # 0.11.2
2
+
3
+ ## Bugfixes
4
+
5
+ The `:cookies` plugin wasn't able to parse `Expires` values, as it was using `Time.httpdate` to parse timestamps, which is RFC 2616-compliant, whereas cookies datetime values need to be RFC 6265-compliant.
@@ -0,0 +1,5 @@
1
+ # 0.11.3
2
+
3
+ ## Bugfixes
4
+
5
+ `EOFError` was being thrown when HTTP/1.1 non-chunked responses which don't contain a "content-length" header. The RFC mandates that the socket should be consumed until the server closes it, so a patch was implemented, to return the available response in such cases.
@@ -0,0 +1,55 @@
1
+ # 0.12.0
2
+
3
+ ## Features
4
+
5
+ ### AWS Sigv4 Authentication Plugin
6
+
7
+ A new plugin, `:aws_sigv4`, is now shipped with `httpx`. It implements the [AWS Signature Version 4 request signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html), a well documented way of authenticating requests to AWS services, which has since been adopted by other cloud providers, such as Google Cloud Storage.
8
+
9
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#sessionaws_sigv4_authentication
10
+
11
+ For convenience, there's a derivative plugin, `:aws_sdk_authentication`, which builds on top of `:aws_sigv4`, and integrates with the `aws-sdk-core` gem, maintained by AWS, to resolve the authentication credentials (p.ex. if you support ephemeral access keys).
12
+
13
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#sessionaws_sdk_authentication
14
+
15
+ Other FAQ: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#faqs
16
+
17
+ ### HTTP/2 support for JRuby
18
+
19
+ `jruby-openssl` doesn't support ALPN protocol negotiation, nor are there plans to implement, which limited the seamless HTTP/2 usage in `httpx`. A new connection adapter was therefore added specifically for JRuby, where ssl/tls connections will be handled using ffi-based openssl bindings, provided you bundle `ffi-compiler` and `concurrent-ruby`, and install a TLS/1.2-compatible `openssl` package.
20
+
21
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/JRuby-Truffleruby-Other-Rubies#http2
22
+
23
+ ## Improvements
24
+
25
+
26
+ ### truffleruby support
27
+
28
+ `httpx` supports and tests against `truffleruby` (known to run tests since v20.3, passing all tests since v21).
29
+
30
+ ### Performance
31
+
32
+ Several optimizations were introduced:
33
+
34
+ * Reduction in read/write system calls;
35
+ * more usage of `String#byteslice` in parsing (instead of string mutation);
36
+ * Avoid selection on connections with no outstanding requests;
37
+
38
+ They all contributed to a massive performance improvement, itself reflected in test runs, which need half the time they used to to complete.
39
+
40
+ ### APIs
41
+
42
+ * `HTTPX::ErrorResponse#to_s` now uses the exception full message, instead of just the backtrace.
43
+
44
+ ## Bugfixes
45
+
46
+ * HTTP/2 stream protocol errors do not cause the process to hang (instead, error responnses are yielded);
47
+ * Fixed body stream bugs on retries when error causing retry would happen mid-transfer;
48
+ * Fixed `:multipart` plugin body rewind on retries to start the transfer from the beginning;
49
+ * Fixed auto-load of `:proxy` plugin when `HTTPS_PROXY` or `HTTP_PROXY` is set;
50
+ * Errno::EPIPE errors mid transfer now cause `httpx` to read from the server and get the appropriate HTTP error response;
51
+ * Make sure that all requests have an error responnse if the error happens early;
52
+ * Fixed TCP handshake Errno::INPROGRESS handling inside TLS connnections, which was causing the process to hang in a high handshake contention scenario;
53
+ * Do not call the event loop if there's nothing to listen on (the DoH resolver was being listened on even if there was nothing to be request);
54
+ * Fixed double event registry for DoH resolvers;
55
+ *
@@ -0,0 +1,58 @@
1
+ # 0.13.0
2
+
3
+ ## Features
4
+
5
+ ### Upgrade plugin
6
+
7
+ A new plugin, `:upgrade`, is now available. This plugin allows one to "hook" on HTTP/1.1's protocol upgrade mechanism (see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism), which is the mechanism that browsers use to initiate websockets (there is an example of how to use `httpx` to start a websocket client connection [in the tests](https://gitlab.com/honeyryderchuck/httpx/-/blob/master/test/support/requests/plugins/upgrade.rb))
8
+
9
+ You can read more about the `:upgrade` plugin in the [wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade).
10
+
11
+ It's the basis of two plugins:
12
+
13
+ #### `:h2c`
14
+
15
+ This plugin was been rewritten on top of the `:upgrade` plugin, and handles upgrading a plaintext (non-"https") HTTP/1.1 connection, into an HTTP/2 connection.
16
+
17
+ https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade#h2c
18
+
19
+ #### `:upgrade/h2`
20
+
21
+ This plugin handles when a server responds to a request with an `Upgrade: h2` header, does the following requests to the same origin via HTTP/2 prior knowledge (bypassing the necessity for ALPN negotiation, which is the whole point of the feature).
22
+
23
+ https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade#h2
24
+
25
+ ### `:addresses` option
26
+
27
+ The `:addresses` option is now available. You can use it to pass a list of IPs to connect to:
28
+
29
+ ```ruby
30
+ # will not resolve example.com, and instead connect to one of the IPs passed.
31
+ HTTPX.get("http://example.com", addresses: %w[172.5.3.1 172.5.3.2]))
32
+ ```
33
+
34
+ You should also use it to connect to HTTP servers bound to a UNIX socket, in which case you'll have to provide a path:
35
+
36
+ ```ruby
37
+ HTTPX.get("http://example.com", addresses: %w[/path/to/usocket]))
38
+ ```
39
+
40
+ The `:transport_options` are therefore deprecated, and will be moved in a major version.
41
+
42
+ ## Improvements
43
+
44
+ Some internal improvements that allow certainn plugins not to "leak" globally, such as the `:compression` plugin, which used to enable compression for all the `httpx` sessions from the same process. It doesn't anymore.
45
+
46
+ Using exceptionless nonblocking connect calls in the supported rubies.
47
+
48
+ Removed unneeded APIs around the Options object (`with_` methods, or the defined options list).
49
+
50
+ ## Bugfixes
51
+
52
+ HTTP/1.1 persistent connections were closing after each request after the max requests was reached. It's fixed, and the new connection will also be persistent.
53
+
54
+ When passing open IO objects for origins (the `:io` option), `httpx` was still trying to resolve the origin's domain. This not only didn't make sense, it broke if the domain is unresolvable. It has been fixed.
55
+
56
+ Fixed usage of `:io` option when passed an "authority/io" hash.
57
+
58
+ Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
@@ -0,0 +1,5 @@
1
+ # 0.13.1
2
+
3
+ ## Bugfixes
4
+
5
+ Rescue `Errno::EALREADY` on calls to `connect_nonblock(exception: false)` (there are exceptionns after all...).
data/lib/httpx.rb CHANGED
@@ -19,7 +19,6 @@ require "httpx/headers"
19
19
  require "httpx/request"
20
20
  require "httpx/response"
21
21
  require "httpx/chainable"
22
- require "httpx/session"
23
22
 
24
23
  # Top-Level Namespace
25
24
  #
@@ -59,3 +58,5 @@ module HTTPX
59
58
 
60
59
  extend Chainable
61
60
  end
61
+
62
+ require "httpx/session"
@@ -123,11 +123,9 @@ module Faraday
123
123
  end
124
124
 
125
125
  def method_missing(meth, *args, &blk)
126
- if @env && @env.respond_to?(meth)
127
- @env.__send__(meth, *args, &blk)
128
- else
129
- super
130
- end
126
+ return super unless @env && @env.respond_to?(meth)
127
+
128
+ @env.__send__(meth, *args, &blk)
131
129
  end
132
130
  end
133
131
 
@@ -197,7 +195,7 @@ module Faraday
197
195
  response_headers.merge!(response.headers)
198
196
  end
199
197
  @app.call(env)
200
- rescue OpenSSL::SSL::SSLError => e
198
+ rescue ::HTTPX::TLSError => e
201
199
  raise SSL_ERROR, e
202
200
  rescue Errno::ECONNABORTED,
203
201
  Errno::ECONNREFUSED,
data/lib/httpx/altsvc.rb CHANGED
@@ -37,6 +37,7 @@ module HTTPX
37
37
  end
38
38
 
39
39
  def emit(request, response)
40
+ return unless response.respond_to?(:headers)
40
41
  # Alt-Svc
41
42
  return unless response.headers.key?("alt-svc")
42
43
 
@@ -17,12 +17,12 @@ module HTTPX
17
17
  # :nocov:
18
18
  def timeout(**args)
19
19
  warn ":#{__method__} is deprecated, use :with_timeout instead"
20
- branch(default_options.with(timeout: args))
20
+ with(timeout: args)
21
21
  end
22
22
 
23
23
  def headers(headers)
24
24
  warn ":#{__method__} is deprecated, use :with_headers instead"
25
- branch(default_options.with(headers: headers))
25
+ with(headers: headers)
26
26
  end
27
27
  # :nocov:
28
28
 
@@ -71,6 +71,8 @@ module HTTPX
71
71
  @inflight = 0
72
72
  @keep_alive_timeout = options.timeout.keep_alive_timeout
73
73
  @keep_alive_timer = nil
74
+
75
+ self.addresses = options.addresses if options.addresses
74
76
  end
75
77
 
76
78
  # this is a semi-private method, to be used by the resolver
@@ -105,6 +107,8 @@ module HTTPX
105
107
 
106
108
  return false if exhausted?
107
109
 
110
+ return false unless connection.addresses
111
+
108
112
  !(@io.addresses & connection.addresses).empty? && @options == connection.options
109
113
  end
110
114
 
@@ -170,7 +174,7 @@ module HTTPX
170
174
  end
171
175
 
172
176
  # if the write buffer is full, we drain it
173
- return :w if @write_buffer.full?
177
+ return :w unless @write_buffer.empty?
174
178
 
175
179
  return @parser.interests if @parser
176
180
 
@@ -251,11 +255,18 @@ module HTTPX
251
255
 
252
256
  def consume
253
257
  catch(:called) do
258
+ epiped = false
254
259
  loop do
255
260
  parser.consume
256
261
 
257
- # we exit if there's no more data to process
258
- if @pending.size.zero? && @inflight.zero?
262
+ # we exit if there's no more requests to process
263
+ #
264
+ # this condition takes into account:
265
+ #
266
+ # * the number of inflight requests
267
+ # * the number of pending requests
268
+ # * whether the write buffer has bytes (i.e. for close handshake)
269
+ if @pending.size.zero? && @inflight.zero? && @write_buffer.empty?
259
270
  log(level: 3) { "NO MORE REQUESTS..." }
260
271
  return
261
272
  end
@@ -265,9 +276,17 @@ module HTTPX
265
276
  read_drained = false
266
277
  write_drained = nil
267
278
 
268
- # dread
279
+ #
280
+ # tight read loop.
281
+ #
282
+ # read as much of the socket as possible.
283
+ #
284
+ # this tight loop reads all the data it can from the socket and pipes it to
285
+ # its parser.
286
+ #
269
287
  loop do
270
288
  siz = @io.read(@window_size, @read_buffer)
289
+ log(level: 3, color: :cyan) { "IO READ: #{siz} bytes..." }
271
290
  unless siz
272
291
  ex = EOFError.new("descriptor closed")
273
292
  ex.set_backtrace(caller)
@@ -275,27 +294,53 @@ module HTTPX
275
294
  return
276
295
  end
277
296
 
297
+ # socket has been drained. mark and exit the read loop.
278
298
  if siz.zero?
279
299
  read_drained = @read_buffer.empty?
300
+ epiped = false
280
301
  break
281
302
  end
282
303
 
283
304
  parser << @read_buffer.to_s
284
305
 
306
+ # continue reading if possible.
307
+ break if interests == :w && !epiped
308
+
309
+ # exit the read loop if connection is preparing to be closed
285
310
  break if @state == :closing || @state == :closed
286
311
 
287
- # for HTTP/2, we just want to write goaway frame
288
- end unless @state == :closing
312
+ # exit #consume altogether if all outstanding requests have been dealt with
313
+ return if @pending.size.zero? && @inflight.zero?
314
+ end unless (interests == :w || @state == :closing) && !epiped
289
315
 
290
- # dwrite
316
+ #
317
+ # tight write loop.
318
+ #
319
+ # flush as many bytes as the sockets allow.
320
+ #
291
321
  loop do
322
+ # buffer has been drainned, mark and exit the write loop.
292
323
  if @write_buffer.empty?
293
324
  # we only mark as drained on the first loop
294
325
  write_drained = write_drained.nil? && @inflight.positive?
326
+
295
327
  break
296
328
  end
297
329
 
298
- siz = @io.write(@write_buffer)
330
+ begin
331
+ siz = @io.write(@write_buffer)
332
+ rescue Errno::EPIPE
333
+ # this can happen if we still have bytes in the buffer to send to the server, but
334
+ # the server wants to respond immediately with some message, or an error. An example is
335
+ # when one's uploading a big file to an unintended endpoint, and the server stops the
336
+ # consumption, and responds immediately with an authorization of even method not allowed error.
337
+ # at this point, we have to let the connection switch to read-mode.
338
+ log(level: 2) { "pipe broken, could not flush buffer..." }
339
+ epiped = true
340
+ read_drained = false
341
+ break
342
+ end
343
+ log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
299
344
  unless siz
300
345
  ex = EOFError.new("descriptor closed")
301
346
  ex.set_backtrace(caller)
@@ -303,21 +348,28 @@ module HTTPX
303
348
  return
304
349
  end
305
350
 
351
+ # socket closed for writing. mark and exit the write loop.
306
352
  if siz.zero?
307
353
  write_drained = !@write_buffer.empty?
308
354
  break
309
355
  end
310
356
 
311
- break if @state == :closing || @state == :closed
357
+ # exit write loop if marked to consume from peer, or is closing.
358
+ break if interests == :r || @state == :closing || @state == :closed
312
359
 
313
360
  write_drained = false
314
- end
361
+ end unless interests == :r
315
362
 
316
363
  # return if socket is drained
317
- if read_drained && write_drained
318
- log(level: 3) { "WAITING FOR EVENTS..." }
319
- return
320
- end
364
+ next unless (interests != :r || read_drained) &&
365
+ (interests != :w || write_drained)
366
+
367
+ # gotta go back to the event loop. It happens when:
368
+ #
369
+ # * the socket is drained of bytes or it's not the interest of the conn to read;
370
+ # * theres nothing more to write, or it's not in the interest of the conn to write;
371
+ log(level: 3) { "(#{interests}): WAITING FOR EVENTS..." }
372
+ return
321
373
  end
322
374
  end
323
375
  end
@@ -424,33 +476,33 @@ module HTTPX
424
476
  remove_instance_variable(:@total_timeout)
425
477
  end
426
478
 
427
- @io.close if @io
428
- @read_buffer.clear
429
- if @keep_alive_timer
430
- @keep_alive_timer.cancel
431
- remove_instance_variable(:@keep_alive_timer)
432
- end
433
-
434
- remove_instance_variable(:@timeout) if defined?(@timeout)
479
+ purge_after_closed
435
480
  when :already_open
436
481
  nextstate = :open
437
482
  send_pending
438
483
  end
439
484
  @state = nextstate
440
- rescue Errno::EHOSTUNREACH
441
- # at this point, all addresses from the IO object have failed
442
- reset
443
- emit(:unreachable)
444
- throw(:jump_tick)
445
485
  rescue Errno::ECONNREFUSED,
446
486
  Errno::EADDRNOTAVAIL,
447
- OpenSSL::SSL::SSLError => e
487
+ Errno::EHOSTUNREACH,
488
+ TLSError => e
448
489
  # connect errors, exit gracefully
449
490
  handle_error(e)
450
491
  @state = :closed
451
492
  emit(:close)
452
493
  end
453
494
 
495
+ def purge_after_closed
496
+ @io.close if @io
497
+ @read_buffer.clear
498
+ if @keep_alive_timer
499
+ @keep_alive_timer.cancel
500
+ remove_instance_variable(:@keep_alive_timer)
501
+ end
502
+
503
+ remove_instance_variable(:@timeout) if defined?(@timeout)
504
+ end
505
+
454
506
  def handle_response
455
507
  @inflight -= 1
456
508
  return unless @inflight.zero?
@@ -10,7 +10,7 @@ module HTTPX
10
10
  MAX_REQUESTS = 100
11
11
  CRLF = "\r\n"
12
12
 
13
- attr_reader :pending
13
+ attr_reader :pending, :requests
14
14
 
15
15
  def initialize(buffer, options)
16
16
  @options = Options.new(options)
@@ -74,9 +74,10 @@ module HTTPX
74
74
  end
75
75
 
76
76
  def consume
77
- requests_limit = [@max_concurrent_requests, @max_requests, @requests.size].min
77
+ requests_limit = [@max_requests, @requests.size].min
78
+ concurrent_requests_limit = [@max_concurrent_requests, requests_limit].min
78
79
  @requests.each_with_index do |request, idx|
79
- break if idx >= requests_limit
80
+ break if idx >= concurrent_requests_limit
80
81
  next if request.state == :done
81
82
 
82
83
  request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
@@ -115,7 +116,7 @@ module HTTPX
115
116
  response = @request.response
116
117
  log(level: 2) { "trailer headers received" }
117
118
 
118
- log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
119
+ log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v.join(", ")}" }.join("\n") }
119
120
  response.merge_headers(h)
120
121
  end
121
122
 
@@ -161,6 +162,16 @@ module HTTPX
161
162
  end
162
163
 
163
164
  def handle_error(ex)
165
+ if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request && @request.response &&
166
+ !@request.response.headers.key?("content-length") &&
167
+ !@request.response.headers.key?("transfer-encoding")
168
+ # if the response does not contain a content-length header, the server closing the
169
+ # connnection is the indicator of response consumed.
170
+ # https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.4
171
+ catch(:called) { on_complete }
172
+ return
173
+ end
174
+
164
175
  if @pipelining
165
176
  disable
166
177
  else
@@ -224,6 +235,8 @@ module HTTPX
224
235
 
225
236
  def disable_pipelining
226
237
  return if @requests.empty?
238
+ # do not disable pipelining if already set to 1 request at a time
239
+ return if @max_concurrent_requests == 1
227
240
 
228
241
  @requests.each do |r|
229
242
  r.transition(:idle)
@@ -240,7 +253,7 @@ module HTTPX
240
253
  @pipelining = false
241
254
  end
242
255
 
243
- def set_request_headers(request)
256
+ def set_protocol_headers(request)
244
257
  request.headers["host"] ||= request.authority
245
258
  request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
246
259
  if !request.headers.key?("content-length") &&
@@ -254,7 +267,6 @@ module HTTPX
254
267
  end
255
268
 
256
269
  def handle(request)
257
- set_request_headers(request)
258
270
  catch(:buffer_full) do
259
271
  request.transition(:headers)
260
272
  join_headers(request) if request.state == :headers
@@ -270,6 +282,7 @@ module HTTPX
270
282
  log(color: :yellow) { "<- HEADLINE: #{buffer.chomp.inspect}" }
271
283
  @buffer << buffer
272
284
  buffer.clear
285
+ set_protocol_headers(request)
273
286
  request.headers.each do |field, value|
274
287
  buffer << "#{capitalized(field)}: #{value}" << CRLF
275
288
  log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }