httpx 0.11.3 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_10_1.md +1 -1
  4. data/doc/release_notes/0_11_1.md +5 -1
  5. data/doc/release_notes/0_12_0.md +55 -0
  6. data/doc/release_notes/0_13_0.md +58 -0
  7. data/doc/release_notes/0_13_1.md +5 -0
  8. data/doc/release_notes/0_13_2.md +9 -0
  9. data/doc/release_notes/0_14_0.md +79 -0
  10. data/lib/httpx.rb +3 -3
  11. data/lib/httpx/adapters/faraday.rb +4 -6
  12. data/lib/httpx/altsvc.rb +1 -0
  13. data/lib/httpx/callbacks.rb +12 -3
  14. data/lib/httpx/chainable.rb +2 -2
  15. data/lib/httpx/connection.rb +92 -37
  16. data/lib/httpx/connection/http1.rb +37 -19
  17. data/lib/httpx/connection/http2.rb +82 -31
  18. data/lib/httpx/headers.rb +1 -1
  19. data/lib/httpx/io.rb +16 -3
  20. data/lib/httpx/io/ssl.rb +35 -24
  21. data/lib/httpx/io/tcp.rb +50 -28
  22. data/lib/httpx/io/tls.rb +218 -0
  23. data/lib/httpx/io/tls/box.rb +365 -0
  24. data/lib/httpx/io/tls/context.rb +199 -0
  25. data/lib/httpx/io/tls/ffi.rb +390 -0
  26. data/lib/httpx/io/udp.rb +31 -7
  27. data/lib/httpx/io/unix.rb +27 -12
  28. data/lib/httpx/options.rb +97 -74
  29. data/lib/httpx/parser/http1.rb +4 -4
  30. data/lib/httpx/plugins/aws_sdk_authentication.rb +84 -0
  31. data/lib/httpx/plugins/aws_sigv4.rb +219 -0
  32. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  33. data/lib/httpx/plugins/compression.rb +24 -12
  34. data/lib/httpx/plugins/compression/brotli.rb +10 -7
  35. data/lib/httpx/plugins/compression/deflate.rb +8 -10
  36. data/lib/httpx/plugins/compression/gzip.rb +4 -3
  37. data/lib/httpx/plugins/cookies.rb +3 -7
  38. data/lib/httpx/plugins/digest_authentication.rb +5 -5
  39. data/lib/httpx/plugins/expect.rb +6 -6
  40. data/lib/httpx/plugins/follow_redirects.rb +4 -4
  41. data/lib/httpx/plugins/grpc.rb +247 -0
  42. data/lib/httpx/plugins/grpc/call.rb +62 -0
  43. data/lib/httpx/plugins/grpc/message.rb +85 -0
  44. data/lib/httpx/plugins/h2c.rb +43 -58
  45. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  46. data/lib/httpx/plugins/multipart.rb +2 -0
  47. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  48. data/lib/httpx/plugins/multipart/part.rb +1 -1
  49. data/lib/httpx/plugins/proxy.rb +4 -8
  50. data/lib/httpx/plugins/proxy/http.rb +1 -1
  51. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  52. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  53. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  54. data/lib/httpx/plugins/push_promise.rb +3 -2
  55. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  56. data/lib/httpx/plugins/retries.rb +15 -16
  57. data/lib/httpx/plugins/stream.rb +99 -77
  58. data/lib/httpx/plugins/upgrade.rb +84 -0
  59. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  60. data/lib/httpx/pool.rb +14 -6
  61. data/lib/httpx/registry.rb +1 -7
  62. data/lib/httpx/request.rb +36 -3
  63. data/lib/httpx/resolver/https.rb +3 -11
  64. data/lib/httpx/resolver/native.rb +7 -3
  65. data/lib/httpx/response.rb +18 -7
  66. data/lib/httpx/selector.rb +5 -0
  67. data/lib/httpx/session.rb +41 -8
  68. data/lib/httpx/transcoder/body.rb +3 -5
  69. data/lib/httpx/transcoder/chunker.rb +1 -1
  70. data/lib/httpx/version.rb +1 -1
  71. data/sig/callbacks.rbs +2 -0
  72. data/sig/chainable.rbs +2 -1
  73. data/sig/connection/http1.rbs +7 -2
  74. data/sig/connection/http2.rbs +10 -4
  75. data/sig/options.rbs +16 -22
  76. data/sig/plugins/aws_sdk_authentication.rbs +19 -0
  77. data/sig/plugins/aws_sigv4.rbs +64 -0
  78. data/sig/plugins/basic_authentication.rbs +2 -0
  79. data/sig/plugins/compression.rbs +7 -5
  80. data/sig/plugins/compression/brotli.rbs +1 -1
  81. data/sig/plugins/compression/deflate.rbs +1 -1
  82. data/sig/plugins/compression/gzip.rbs +1 -1
  83. data/sig/plugins/cookies.rbs +0 -1
  84. data/sig/plugins/digest_authentication.rbs +0 -1
  85. data/sig/plugins/expect.rbs +0 -2
  86. data/sig/plugins/follow_redirects.rbs +0 -2
  87. data/sig/plugins/h2c.rbs +5 -10
  88. data/sig/plugins/persistent.rbs +0 -1
  89. data/sig/plugins/proxy.rbs +0 -1
  90. data/sig/plugins/push_promise.rbs +1 -1
  91. data/sig/plugins/retries.rbs +0 -4
  92. data/sig/plugins/stream.rbs +17 -16
  93. data/sig/plugins/upgrade.rbs +23 -0
  94. data/sig/request.rbs +7 -2
  95. data/sig/response.rbs +4 -1
  96. data/sig/session.rbs +4 -0
  97. metadata +56 -33
  98. data/lib/httpx/timeout.rb +0 -67
  99. data/sig/timeout.rbs +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1612c9f4696a7a6ae0508ea66f864654e28470e742b91b57cd14ab5b43adad2
4
- data.tar.gz: 791569d7282f7cb3e451d245e332871c170e4571ca5e10ead757f93afffe3d78
3
+ metadata.gz: abbeaccc55115244f08e7b39aaad174a34dd2e7aeb10b32cdc0563fbefe1b953
4
+ data.tar.gz: a39fc4e5644b21c1265840011e969c36904756573584cba0f805d568a549ec94
5
5
  SHA512:
6
- metadata.gz: 96e8bc5d07f59b21ccdbc54bb11bce9b1deb53eb128c4d6c2652a0dfca6c925cbe084e82f352857004420cc5753fbacc3a68e2bc4d885937a2afbfa5c6138836
7
- data.tar.gz: b74cbac2346d110a97e6ab9a13f2a62db7ed23ce1454df510f9c8cb6ea58314edff2724b9cc269cdf8d5fdf00e9ce6c5c28eff864605f6ce01a9e2f4b9078911
6
+ metadata.gz: 82fa475dcd9ef05ebe90f0b3c742c17c33185410a70eb5af730fce393681b7f9f346decf78349614f77b76257550c0e3056e61f3b5d6cd5efdba0149eff235f6
7
+ data.tar.gz: 64ceb596415b440c99c9df31569fa0ee2f7c94e51787d5e0d84141f1ade0be92e23e2d7011a361c1366b43c4f159525599367009bd4021c358537f11c4605cac
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
 
@@ -26,7 +26,7 @@ From now on, both headers and the responnse payload will also appear, so expecte
26
26
  ## Bugfixes
27
27
 
28
28
  * HTTP/2 and HTTP/1.1 exhausted connections now get properly migrated into a new connection;
29
- * HTTP/2 421 responses will now correctly migrate the connection and pendign requests to HTTP/1.1 (a hanging loop was being caused);
29
+ * HTTP/2 421 responses will now correctly migrate the connection and pending requests to HTTP/1.1 (a hanging loop was being caused);
30
30
  * HTTP/2 connection failed with a GOAWAY settings timeout will now return error responses (instead of hanging indefinitely);
31
31
  * Non-IP proxy name-resolving errors will now move on to the next available proxy in the list (instead of hanging indefinitely);
32
32
  * Non-IP DNS resolve errors for `native` and `https` variants will now return the appropriate error response (instead of hanging indefinitely);
@@ -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,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 certain 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 exceptions after all...).
@@ -0,0 +1,9 @@
1
+ # 0.13.1
2
+
3
+ ## Improvements
4
+
5
+ `UDPSocket#sendmsg_nonblock` is now used in the native resolver.
6
+
7
+ ## Bugfixes
8
+
9
+ Usage in Windows was buggy, resulting in `Errno::EINVAL` during DNS resolving, when using the native resolver. This was due to a discrepancy between `recvfrom` behaviour in WS Sockets and Linux Sockets. This was fixed by making we the UDP socket never tries to receive before a DNS query has been actually sent.
@@ -0,0 +1,79 @@
1
+ # 0.14.0
2
+
3
+ ## Features
4
+
5
+ ### GRPC plugin
6
+
7
+ A new plugin, `:grpc`, is now available. This plugin provides a simple DSL to build GRPC services and performing calls using `httpx` under the hood.
8
+
9
+ Example:
10
+
11
+ ```ruby
12
+ require "httpx"
13
+
14
+ grpc = HTTPX.plugin(:grpc)
15
+ helloworld_stub = grpc.build_stub("localhost:4545")
16
+ helloworld_svc = helloworld_stub.rpc(:SayHello, HelloRequest, HelloReply)
17
+ result = helloworld_svc.say_hello(HelloRequest.new(name: "Jack")) #=> HelloReply: "Hello Jack"
18
+ ```
19
+
20
+ You can read more about the `:grpc` plugin in the [wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/GRPC).
21
+
22
+ ### :origin
23
+
24
+ A new `:origin` option is available. You can use it for setting a base URL for subsequent relative paths on that session:
25
+
26
+ ```ruby
27
+ HTTPX.get("/httpbin/get") #=> HTTPX::Error: invalid URI: /httpbin/get
28
+
29
+ httpbin = HTTPX.with(origin: "https://nghttp2.org")
30
+ httpbin.get("/httpbin/get") #=> #<Response:5420 HTTP/2.0 @status=200 ....
31
+ ```
32
+
33
+ **Note!** The origin is **not** for setting base paths, i.e. if you pass it a relative path, it'll be filtered out in subsequent requests (`HTTPX.with(origin: "https://nghttp2.org/httpbin")` will still use only `"https://nghttp2.org"`).
34
+
35
+ ## Improvements
36
+
37
+ * setting an unexpected option will now raise an `HTTPX::Error` with an helpful message, instead of a confusing `NoMethodError`:
38
+
39
+ ```ruby
40
+ HTTPX.with(foo: "bar")
41
+ # before
42
+ #=> NoMethodError
43
+ # after
44
+ #=> HTTPX::Error: unknown option: foo
45
+
46
+ ```
47
+
48
+ * `HTTPX::Options#def_option` (which can be used for setting custom plugin options) can now be passed a full body string (where the argument is `value`), although it still support the block form. This is the recommended approach, as the block form is based on `define_method`, which would make clients unusable inside ractors.
49
+
50
+ * Added support for `:wait_for_handshake` under the `http2_settings` option (`false` by default). HTTP/2 connections complete the protocol handshake before requests are sent. When this option is `true`, requests get send in the initial payload, before the HTTP/2 connection is fully acknowledged.
51
+
52
+ * 441716a5ac0f7707211ebe0048f568cf0b759c3f: The `:stream` plugin has been improved to start streaming the real response as methods are called (instead of a completely separate synchronous one, which is definitely not good):
53
+
54
+ ```ruby
55
+ session = HTTPX.plugin(:stream)
56
+ response = session.get(build_uri("/stream/3"), stream: true)
57
+
58
+ # before
59
+ response.status # this could block indefinitely, if the request truly streams infinitely.
60
+
61
+ # after
62
+ response.status # sends the request, and starts streaming the response until status is available.
63
+ response.each {|chunk|...} # and now you can start yielding the chunks...
64
+ ```
65
+
66
+
67
+ ## Bugfixes
68
+
69
+ * fixed usage of the `:multipart` if `pathname` isn't loaded.
70
+ * fixed HTTP/2 trailers.
71
+ * fixed connection merges with the same origin, which was causing them to be duplicated and breaking further usage. (#125)
72
+ * fixed repeated session callbacks on a connection, by ensure they're set only once.
73
+ * fixed calculation of `content-length` for streaming or chunked compressed requests.
74
+
75
+
76
+ ## Chore
77
+
78
+ * using ruby base container images in CI instead.
79
+ * using truffleruby official container image.
data/lib/httpx.rb CHANGED
@@ -12,14 +12,12 @@ require "httpx/callbacks"
12
12
  require "httpx/loggable"
13
13
  require "httpx/registry"
14
14
  require "httpx/transcoder"
15
- require "httpx/options"
16
- require "httpx/timeout"
17
15
  require "httpx/pool"
18
16
  require "httpx/headers"
19
17
  require "httpx/request"
20
18
  require "httpx/response"
19
+ require "httpx/options"
21
20
  require "httpx/chainable"
22
- require "httpx/session"
23
21
 
24
22
  # Top-Level Namespace
25
23
  #
@@ -59,3 +57,5 @@ module HTTPX
59
57
 
60
58
  extend Chainable
61
59
  end
60
+
61
+ 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
 
@@ -6,19 +6,28 @@ module HTTPX
6
6
  callbacks(type) << action
7
7
  end
8
8
 
9
- def once(event, &block)
10
- on(event) do |*args, &callback|
9
+ def once(type, &block)
10
+ on(type) do |*args, &callback|
11
11
  block.call(*args, &callback)
12
12
  :delete
13
13
  end
14
14
  end
15
15
 
16
+ def only(type, &block)
17
+ callbacks(type).clear
18
+ on(type, &block)
19
+ end
20
+
16
21
  def emit(type, *args)
17
- callbacks(type).delete_if { |pr| pr[*args] == :delete }
22
+ callbacks(type).delete_if { |pr| :delete == pr[*args] } # rubocop:disable Style/YodaCondition
18
23
  end
19
24
 
20
25
  protected
21
26
 
27
+ def callbacks_for?(type)
28
+ @callbacks.key?(type) && !@callbacks[type].empty?
29
+ end
30
+
22
31
  def callbacks(type = nil)
23
32
  return @callbacks unless type
24
33
 
@@ -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
 
@@ -69,8 +69,10 @@ module HTTPX
69
69
  end
70
70
 
71
71
  @inflight = 0
72
- @keep_alive_timeout = options.timeout.keep_alive_timeout
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
 
@@ -125,7 +129,7 @@ module HTTPX
125
129
  end
126
130
 
127
131
  def merge(connection)
128
- @origins += connection.instance_variable_get(:@origins)
132
+ @origins |= connection.instance_variable_get(:@origins)
129
133
  connection.purge_pending do |req|
130
134
  send(req)
131
135
  end
@@ -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
 
@@ -234,9 +238,9 @@ module HTTPX
234
238
  def timeout
235
239
  return @timeout if defined?(@timeout)
236
240
 
237
- return @options.timeout.connect_timeout if @state == :idle
241
+ return @options.timeout[:connect_timeout] if @state == :idle
238
242
 
239
- @options.timeout.operation_timeout
243
+ @options.timeout[:operation_timeout]
240
244
  end
241
245
 
242
246
  private
@@ -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
@@ -361,7 +413,7 @@ module HTTPX
361
413
  emit(:exhausted)
362
414
  end
363
415
  parser.on(:origin) do |origin|
364
- @origins << origin
416
+ @origins |= [origin]
365
417
  end
366
418
  parser.on(:close) do |force|
367
419
  transition(:closing)
@@ -391,6 +443,7 @@ module HTTPX
391
443
  emit(:misdirected, request)
392
444
  else
393
445
  response = ErrorResponse.new(request, ex, @options)
446
+ request.response = response
394
447
  request.emit(:response, response)
395
448
  end
396
449
  end
@@ -399,7 +452,7 @@ module HTTPX
399
452
  def transition(nextstate)
400
453
  case nextstate
401
454
  when :idle
402
- @timeout = @current_timeout = @options.timeout.connect_timeout
455
+ @timeout = @current_timeout = @options.timeout[:connect_timeout]
403
456
 
404
457
  when :open
405
458
  return if @state == :closed
@@ -411,7 +464,7 @@ module HTTPX
411
464
 
412
465
  send_pending
413
466
 
414
- @timeout = @current_timeout = @options.timeout.operation_timeout
467
+ @timeout = @current_timeout = @options.timeout[:operation_timeout]
415
468
  emit(:open)
416
469
  when :closing
417
470
  return unless @state == :open
@@ -424,33 +477,33 @@ module HTTPX
424
477
  remove_instance_variable(:@total_timeout)
425
478
  end
426
479
 
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)
480
+ purge_after_closed
435
481
  when :already_open
436
482
  nextstate = :open
437
483
  send_pending
438
484
  end
439
485
  @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
486
  rescue Errno::ECONNREFUSED,
446
487
  Errno::EADDRNOTAVAIL,
447
- OpenSSL::SSL::SSLError => e
488
+ Errno::EHOSTUNREACH,
489
+ TLSError => e
448
490
  # connect errors, exit gracefully
449
491
  handle_error(e)
450
492
  @state = :closed
451
493
  emit(:close)
452
494
  end
453
495
 
496
+ def purge_after_closed
497
+ @io.close if @io
498
+ @read_buffer.clear
499
+ if @keep_alive_timer
500
+ @keep_alive_timer.cancel
501
+ remove_instance_variable(:@keep_alive_timer)
502
+ end
503
+
504
+ remove_instance_variable(:@timeout) if defined?(@timeout)
505
+ end
506
+
454
507
  def handle_response
455
508
  @inflight -= 1
456
509
  return unless @inflight.zero?
@@ -490,12 +543,14 @@ module HTTPX
490
543
  def handle_error(error)
491
544
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
492
545
  while (request = @pending.shift)
493
- request.emit(:response, ErrorResponse.new(request, error, @options))
546
+ response = ErrorResponse.new(request, error, @options)
547
+ request.response = response
548
+ request.emit(:response, response)
494
549
  end
495
550
  end
496
551
 
497
552
  def total_timeout
498
- total = @options.timeout.total_timeout
553
+ total = @options.timeout[:total_timeout]
499
554
 
500
555
  return unless total
501
556