httpx 0.20.5 → 0.21.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -26
  3. data/doc/release_notes/0_13_0.md +1 -1
  4. data/doc/release_notes/0_21_0.md +96 -0
  5. data/doc/release_notes/0_21_1.md +12 -0
  6. data/lib/httpx/connection/http1.rb +2 -1
  7. data/lib/httpx/connection.rb +47 -3
  8. data/lib/httpx/errors.rb +18 -0
  9. data/lib/httpx/extensions.rb +8 -4
  10. data/lib/httpx/io/unix.rb +1 -1
  11. data/lib/httpx/options.rb +7 -3
  12. data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
  13. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
  14. data/lib/httpx/plugins/circuit_breaker.rb +115 -0
  15. data/lib/httpx/plugins/cookies.rb +1 -1
  16. data/lib/httpx/plugins/expect.rb +1 -1
  17. data/lib/httpx/plugins/multipart/decoder.rb +1 -1
  18. data/lib/httpx/plugins/proxy.rb +7 -1
  19. data/lib/httpx/plugins/retries.rb +1 -1
  20. data/lib/httpx/plugins/webdav.rb +78 -0
  21. data/lib/httpx/request.rb +15 -25
  22. data/lib/httpx/resolver/https.rb +2 -7
  23. data/lib/httpx/resolver/native.rb +19 -8
  24. data/lib/httpx/response.rb +27 -9
  25. data/lib/httpx/timers.rb +3 -0
  26. data/lib/httpx/transcoder/form.rb +1 -1
  27. data/lib/httpx/transcoder/json.rb +19 -3
  28. data/lib/httpx/transcoder/xml.rb +57 -0
  29. data/lib/httpx/transcoder.rb +1 -0
  30. data/lib/httpx/version.rb +1 -1
  31. data/sig/buffer.rbs +1 -1
  32. data/sig/chainable.rbs +1 -0
  33. data/sig/connection.rbs +12 -4
  34. data/sig/errors.rbs +13 -0
  35. data/sig/io.rbs +6 -0
  36. data/sig/options.rbs +4 -1
  37. data/sig/plugins/circuit_breaker.rbs +61 -0
  38. data/sig/plugins/compression/brotli.rbs +1 -1
  39. data/sig/plugins/compression/deflate.rbs +1 -1
  40. data/sig/plugins/compression/gzip.rbs +3 -3
  41. data/sig/plugins/compression.rbs +1 -1
  42. data/sig/plugins/multipart.rbs +1 -1
  43. data/sig/plugins/proxy/socks5.rbs +3 -2
  44. data/sig/plugins/proxy.rbs +1 -1
  45. data/sig/registry.rbs +5 -4
  46. data/sig/request.rbs +7 -1
  47. data/sig/resolver/native.rbs +5 -2
  48. data/sig/response.rbs +3 -1
  49. data/sig/timers.rbs +1 -1
  50. data/sig/transcoder/json.rbs +4 -1
  51. data/sig/transcoder/xml.rbs +21 -0
  52. data/sig/transcoder.rbs +2 -2
  53. data/sig/utils.rbs +2 -2
  54. metadata +15 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47766fc5b4fb9cb70e7b0feb8333656b987a25aea262c645c36de53e7cc3f464
4
- data.tar.gz: 2f49417d21841b803d9f44b76c199785a54b7983c7712cf8a39b4837900e892b
3
+ metadata.gz: 1195773336c2fb0b92ed10abfafa8a99c7cfd19af31b932dbfff72d073ed3f47
4
+ data.tar.gz: 634e9e62edcd8184d014b1870cbc303cdfcc8035e0c76431b10c1e9143bdbb23
5
5
  SHA512:
6
- metadata.gz: f74fd61a16e738fbe668312e14d7c658af062f4f3dbcf65360b33892e27f31a524e7b675136de5e7693e327ba9a388c74c995e43b4e49a4593ec63fa385d07df
7
- data.tar.gz: b04ae701b05663bd8c94d74416fd6a124d3a700e5860065d373f452383cc6f48a5569e8696dfac5a7f398824c163b3ddb62553a56c465022dc35ac34e627a479
6
+ metadata.gz: bee27d21eef64771e054b4517e99371462cc28fb1c5cad4353137e111e96a4d22131d573aa5c31fe6b86c7007466feec5d1181ae6787a3a6ef87459468d911c8
7
+ data.tar.gz: 15fa97c4456ce2d8f875590075c9ceb778096aca70d27a793c5cafa026f038331439ac62fd823210290013c0466c072a2eff5c1a0a06b1c880254e542d3f9e16
data/README.md CHANGED
@@ -19,13 +19,17 @@ And also:
19
19
 
20
20
  * Compression (gzip, deflate, brotli)
21
21
  * Streaming Requests
22
- * Authentication (Basic Auth, Digest Auth)
22
+ * Authentication (Basic Auth, Digest Auth, NTLM)
23
23
  * Expect 100-continue
24
24
  * Multipart Requests
25
- * Cookies
25
+ * Advanced Cookie handling
26
26
  * HTTP/2 Server Push
27
- * H2C Upgrade
27
+ * HTTP/1.1 Upgrade (support for "h2c", "h2")
28
28
  * Automatic follow redirects
29
+ * GRPC
30
+ * WebDAV
31
+ * Circuit Breaker
32
+ * HTTP-based response cache
29
33
  * International Domain Names
30
34
 
31
35
  ## How
@@ -36,7 +40,14 @@ Here are some simple examples:
36
40
  HTTPX.get("https://nghttp2.org").to_s #=> "<!DOCT...."
37
41
  ```
38
42
 
39
- And that's the simplest one there is.
43
+ And that's the simplest one there is. But you can also do:
44
+
45
+ ```ruby
46
+ HTTPX.post("http://example.com", form: { user: "john", password: "pass" })
47
+
48
+ http = HTTPX.with(headers: { "x-my-name" => "joe" })
49
+ http.patch(("http://example.com/file", body: File.open("path/to/file")) # request body is streamed
50
+ ```
40
51
 
41
52
  If you want to do some more things with the response, you can get an `HTTPX::Response`:
42
53
 
@@ -50,7 +61,7 @@ puts body #=> #<HTTPX::Response ...
50
61
  You can also send as many requests as you want simultaneously:
51
62
 
52
63
  ```ruby
53
- page1, page2, page3 = HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
64
+ page1, page2, page3 =`HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
54
65
  ```
55
66
 
56
67
  ## Installation
@@ -73,43 +84,53 @@ and then just require it in your program:
73
84
  require "httpx"
74
85
  ```
75
86
 
76
- ## Why Should I care?
87
+ ## What makes it the best ruby HTTP client
77
88
 
78
- In Ruby, HTTP client implementations are a known cheap commodity. Why this one?
79
89
 
80
- ### Concurrency
90
+ ### Concurrency, HTTP/2 support
81
91
 
82
- This library supports HTTP/2 seamlessly (which means, if the request is secure, and the server support ALPN negotiation AND HTTP/2, the request will be made through HTTP/2). If you pass multiple URIs, and they can utilize the same connection, they will run concurrently in it.
92
+ `httpx` supports HTTP/2 (for "https" requests, it'll automatically do ALPN negotiation). However if the server supports HTTP/1.1, it will use HTTP pipelining, falling back to 1 request at a time if the server doesn't support it either (and it'll use Keep-Alive connections, unless the server does not support).
83
93
 
84
- However if the server supports HTTP/1.1, it will try to use HTTP pipelining, falling back to 1 request at a time if the server doesn't support it (if the server support Keep-Alive connections, it will reuse the same connection).
94
+ If you passed multiple URIs, it'll perform all of the requests concurrently, by mulitplexing on the necessary sockets (and it'll batch requests to the same socket when the origin is the same):
95
+
96
+ ```ruby
97
+ HTTPX.get(
98
+ "https://news.ycombinator.com/news",
99
+ "https://news.ycombinator.com/news?p=2",
100
+ "https://google.com/q=me"
101
+ ) # first two requests will be multiplexed on the same socket.
102
+ ```
85
103
 
86
104
  ### Clean API
87
105
 
88
106
  `httpx` builds all functions around the `HTTPX` module, so that all calls can compose of each other. Here are a few examples:
89
107
 
90
108
  ```ruby
91
- response = HTTPX.get("https://www.google.com")
92
- response = HTTPX.post("https://www.nghttp2.org/httpbin/post", params: {name: "John", age: "22"})
109
+ response = HTTPX.get("https://www.google.com", params: { q: "me" })
110
+ response = HTTPX.post("https://www.nghttp2.org/httpbin/post", form: {name: "John", age: "22"})
93
111
  response = HTTPX.plugin(:basic_authentication)
94
112
  .basic_authentication("user", "pass")
95
113
  .get("https://www.google.com")
114
+
115
+ # more complex client objects can be cached, and are thread-safe
116
+ http = HTTPX.plugin(:compression).plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
117
+ http.get("https://example.com") # the above options will apply
118
+ http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
96
119
  ```
97
120
 
98
121
  ### Lightweight
99
122
 
100
- It ships with a plugin system similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/janko-m/shrine).
101
-
102
- It means that it loads the bare minimum to perform requests, and the user has to explicitly load the plugins, in order to get the features he/she needs.
123
+ It ships with most features published as a plugin, making vanilla `httpx` lightweight and dependency-free, while allowing you to "pay for what you use"
103
124
 
104
- It also means that it ships with the minimum amount of dependencies.
125
+ The plugin system is similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/janko-m/shrine).
105
126
 
106
- ### DNS-over-HTTPS
127
+ ### Advanced DNS features
107
128
 
108
- `HTTPX` ships with custom DNS resolver implementations, including a DNS-over-HTTPS resolver.
129
+ `HTTPX` ships with custom DNS resolver implementations, including a native Happy Eyeballs resolver immplementation, and a DNS-over-HTTPS resolver.
109
130
 
110
- ## Easy to test
131
+ ## User-driven test suite
111
132
 
112
- The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/httpbin/), so there are no mocking/stubbing false positives. The test suite uses [minitest](https://github.com/seattlerb/minitest), but its matchers usage is (almost) limited to `#assert` (`assert` is all you need).
133
+ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/httpbin/), so actual requests are performed during tests.
113
134
 
114
135
  ## Supported Rubies
115
136
 
@@ -122,18 +143,15 @@ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
122
143
  | ------------- | --------------------------------------------------- |
123
144
  | Website | https://honeyryderchuck.gitlab.io/httpx/ |
124
145
  | Documentation | https://honeyryderchuck.gitlab.io/httpx/rdoc/ |
125
- | Wiki | https://gitlab.com/honeyryderchuck/httpx/wikis/home |
146
+ | Wiki | https://honeyryderchuck.gitlab.io/httpx/wiki/home.html |
126
147
  | CI | https://gitlab.com/honeyryderchuck/httpx/pipelines |
148
+ | Rubygems | https://rubygems.org/gems/httpx |
127
149
 
128
150
  ## Caveats
129
151
 
130
152
  ### ALPN support
131
153
 
132
- `HTTPS` TLS backend is ruby's own `openssl` gem.
133
-
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
-
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.
154
+ ALPN negotiation is required for "auto" HTTP/2 "https" requests. This is available in ruby since version 2.3 .
137
155
 
138
156
  ### Known bugs
139
157
 
@@ -34,7 +34,7 @@ HTTPX.get("http://example.com", addresses: %w[172.5.3.1 172.5.3.2]))
34
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
35
 
36
36
  ```ruby
37
- HTTPX.get("http://example.com", addresses: %w[/path/to/usocket]))
37
+ HTTPX.get("http://example.com", transport: "unix", addresses: %w[/path/to/usocket]))
38
38
  ```
39
39
 
40
40
  The `:transport_options` are therefore deprecated, and will be moved in a major version.
@@ -0,0 +1,96 @@
1
+ # 0.21.0
2
+
3
+ ## Features
4
+
5
+ ### `:write_timeout`, `:read_timeout` and `:request_timeout`
6
+
7
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts
8
+
9
+ The following timeouts are now supported:
10
+
11
+ * `:write_timeout`: total time (in seconds) to write a request to the server;
12
+ * `:read_timeout`: total time (in seconds) to read a response from the server;
13
+ * `:request_timeout`: tracks both of the above (time to write the request and read a response);
14
+
15
+ ```ruby
16
+ HTTPX.with(timeout: { request_timeout: 60}).get(...
17
+ ```
18
+
19
+ Just like `:connect_timeout`, the new timeouts are deadline-oriented, rather than op-oriented, meaning that they do not reset on each socket operation (as most ruby HTTP clients do).
20
+
21
+ None of them has a default value, in order not to break integrations, but that'll change in a future v1, where they'll become the default timeouts.
22
+
23
+ ### Circuit Breaker plugin
24
+
25
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Circuit-Breaker
26
+
27
+ The `:circuit_breaker` plugin wraps around errors happening when performing HTTP requests, and support options for setting maximum number of attempts before circuit opens (`:circuit_breaker_max_attempts`), period after which attempts should be reset (`:circuit_breaker_reset_attempts_in`), timespan until circuit half-opens (`circuit_breaker_break_in`), respective half-open drip rate (`:circuit_breaker_half_open_drip_rate`), and a callback to do your own check on whether a response has failed, in case you want HTTP level errors to be marked as failed attempts (`:circuit_breaker_break_on`).
28
+
29
+ Read the wiki for more info about the defaults.
30
+
31
+ ```ruby
32
+ http = HTTPX.plugin(:circuit_breaker)
33
+ # that's it!
34
+ http.get(...
35
+ ```
36
+
37
+ ### WebDAV plugin
38
+
39
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/WebDav
40
+
41
+ The `:webdav` introduces some "convenience" methods to perform common WebDAV operations.
42
+
43
+ ```ruby
44
+ webdav = HTTPX.plugin(:webdav, origin: "http://webdav-server")
45
+ .plugin(:digest_authentication).digest_auth("user", "pass")
46
+
47
+ res = webdav.put("/file.html", body: "this is the file body")
48
+ res = webdav.copy("/file.html", "/newdir/copy.html")
49
+ # ...
50
+ ```
51
+
52
+ ### XML transcoder, `:xml` option and `response.xml`
53
+
54
+ A new transcoder was added fot the XML mime type, which requires `"nokogiri"` to be installed. It can both serialize Nokogiri nodes in a request, and parse response content into nokogiri nodes:
55
+
56
+ ```ruby
57
+ response = HTTPX.post("https://xml-server.com", xml: Nokogiri::XML("<xml ..."))
58
+ response.xml #=> #(Document:0x16e4 { name = "document", children = ...
59
+ ```
60
+
61
+ ## Improvements
62
+
63
+ ### `:proxy` plugin: `:no_proxy` option
64
+
65
+ Support was added, in the `:proxy` plugin, to declare domains, either via regexp patterns, or strings, for which requests should bypass the proxy.
66
+
67
+ ```ruby
68
+ http = HTTPX.plugin(:proxy).with_proxy(
69
+ uri: "http://10.10.0.1:51432",
70
+ no_proxy: ["gitlab.local", /*.google.com/]
71
+ )
72
+ http.get("https://duckduckgo.com/?q=httpx") #=> proxied
73
+ http.get("https://google.com/?q=httpx") #=> not proxied
74
+ http.get("https://gitlab.com") #=> proxied
75
+ http.get("https://gitlab.local") #=> not proxied
76
+ ```
77
+
78
+ ### OOTB support for other JSON libraries
79
+
80
+ If one of `multi_json`, `oj` or `yajl` is available, all `httpx` operations doing JSON parsing or dumping will use it (the `json` standard library will be used otherwise).
81
+
82
+ ```ruby
83
+ require "oj"
84
+ require "httpx"
85
+
86
+ response = HTTPX.post("https://somedomain.json", json: { "foo" => "bar" }) # will use "oj"
87
+ puts response.json # will use "oj"
88
+ ```
89
+
90
+ ## Bugfixes
91
+
92
+ * `:expect` plugin: `:expect_timeout` can accept floats (not just integers).
93
+
94
+ ## Chore
95
+
96
+ * DoH `:https` resolver: support was removed for the "application/dns-json" mime-type (it was only supported in practice by the Google DoH resolver, which has since added support for the standardized "application/dns-message").
@@ -0,0 +1,12 @@
1
+ # 0.21.1
2
+
3
+ ## Bugfixes
4
+
5
+ * fix: protecting tcp connect phase against low-level syscall errors
6
+ * such as network unreachable, which can happen if connectivity is lost meanwhile.
7
+ * native resolver: fix for nameserver switch not happening in case of DNS timeout.
8
+ * when more than a nameserver was advertised by the system.
9
+
10
+ ## Chore
11
+
12
+ * Removing usage of deprecated `Random::DEFAULT.rand` (using `Random.rand` instead)-
@@ -118,7 +118,7 @@ module HTTPX
118
118
  log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
119
119
 
120
120
  @request.response = response
121
- on_complete if response.complete?
121
+ on_complete if response.finished?
122
122
  end
123
123
 
124
124
  def on_trailers(h)
@@ -158,6 +158,7 @@ module HTTPX
158
158
  @request = nil
159
159
  @requests.shift
160
160
  response = request.response
161
+ response.finish!
161
162
  emit(:response, request, response)
162
163
 
163
164
  if @parser.upgrade?
@@ -34,6 +34,7 @@ module HTTPX
34
34
  include Callbacks
35
35
 
36
36
  using URIExtensions
37
+ using NumericExtensions
37
38
 
38
39
  require "httpx/connection/http2"
39
40
  require "httpx/connection/http1"
@@ -233,6 +234,7 @@ module HTTPX
233
234
  # when pushing a request into an existing connection, we have to check whether there
234
235
  # is the possibility that the connection might have extended the keep alive timeout.
235
236
  # for such cases, we want to ping for availability before deciding to shovel requests.
237
+ log(level: 3) { "keep alive timeout expired, pinging connection..." }
236
238
  @pending << request
237
239
  parser.ping
238
240
  transition(:active) if @state == :inactive
@@ -430,6 +432,8 @@ module HTTPX
430
432
  @inflight += 1
431
433
  parser.send(request)
432
434
 
435
+ set_request_timeouts(request)
436
+
433
437
  return unless @state == :inactive
434
438
 
435
439
  transition(:active)
@@ -508,9 +512,14 @@ module HTTPX
508
512
 
509
513
  def transition(nextstate)
510
514
  handle_transition(nextstate)
511
- rescue Errno::ECONNREFUSED,
515
+ rescue Errno::ECONNABORTED,
516
+ Errno::ECONNREFUSED,
517
+ Errno::ECONNRESET,
512
518
  Errno::EADDRNOTAVAIL,
513
519
  Errno::EHOSTUNREACH,
520
+ Errno::EINVAL,
521
+ Errno::ENETUNREACH,
522
+ Errno::EPIPE,
514
523
  TLSError => e
515
524
  # connect errors, exit gracefully
516
525
  handle_error(e)
@@ -573,7 +582,7 @@ module HTTPX
573
582
  error = ex
574
583
  else
575
584
  # inactive connections do not contribute to the select loop, therefore
576
- # they should fail due to such errors.
585
+ # they should not fail due to such errors.
577
586
  return if @state == :inactive
578
587
 
579
588
  if @timeout
@@ -591,10 +600,45 @@ module HTTPX
591
600
  def handle_error(error)
592
601
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
593
602
  while (request = @pending.shift)
594
- response = ErrorResponse.new(request, error, @options)
603
+ response = ErrorResponse.new(request, error, request.options)
595
604
  request.response = response
596
605
  request.emit(:response, response)
597
606
  end
598
607
  end
608
+
609
+ def set_request_timeouts(request)
610
+ write_timeout = request.write_timeout
611
+ request.once(:headers) do
612
+ @timers.after(write_timeout) { write_timeout_callback(request, write_timeout) }
613
+ end unless write_timeout.nil? || write_timeout.infinite?
614
+
615
+ read_timeout = request.read_timeout
616
+ request.once(:done) do
617
+ @timers.after(read_timeout) { read_timeout_callback(request, read_timeout) }
618
+ end unless read_timeout.nil? || read_timeout.infinite?
619
+
620
+ request_timeout = request.request_timeout
621
+ request.once(:headers) do
622
+ @timers.after(request_timeout) { read_timeout_callback(request, request_timeout, RequestTimeoutError) }
623
+ end unless request_timeout.nil? || request_timeout.infinite?
624
+ end
625
+
626
+ def write_timeout_callback(request, write_timeout)
627
+ return if request.state == :done
628
+
629
+ @write_buffer.clear
630
+ error = WriteTimeoutError.new(request, nil, write_timeout)
631
+ on_error(error)
632
+ end
633
+
634
+ def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
635
+ response = request.response
636
+
637
+ return if response && response.finished?
638
+
639
+ @write_buffer.clear
640
+ error = error_type.new(request, request.response, read_timeout)
641
+ on_error(error)
642
+ end
599
643
  end
600
644
  end
data/lib/httpx/errors.rb CHANGED
@@ -24,6 +24,24 @@ module HTTPX
24
24
 
25
25
  class ConnectTimeoutError < TimeoutError; end
26
26
 
27
+ class RequestTimeoutError < TimeoutError
28
+ attr_reader :request
29
+
30
+ def initialize(request, response, timeout)
31
+ @request = request
32
+ @response = response
33
+ super(timeout, "Timed out after #{timeout} seconds")
34
+ end
35
+
36
+ def marshal_dump
37
+ [message]
38
+ end
39
+ end
40
+
41
+ class ReadTimeoutError < RequestTimeoutError; end
42
+
43
+ class WriteTimeoutError < RequestTimeoutError; end
44
+
27
45
  class SettingsTimeoutError < TimeoutError; end
28
46
 
29
47
  class ResolveTimeoutError < TimeoutError; end
@@ -54,6 +54,14 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module NumericExtensions
58
+ refine Numeric do
59
+ def infinite?
60
+ self == Float::INFINITY
61
+ end unless Numeric.method_defined?(:infinite?)
62
+ end
63
+ end
64
+
57
65
  module StringExtensions
58
66
  refine String do
59
67
  def delete_suffix!(suffix)
@@ -135,10 +143,6 @@ module HTTPX
135
143
  end
136
144
 
137
145
  module RegexpExtensions
138
- # If you wonder why this is there: the oauth feature uses a refinement to enhance the
139
- # Regexp class locally with #match? , but this is never tested, because ActiveSupport
140
- # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
141
- # :nocov:
142
146
  refine(Regexp) do
143
147
  def match?(*args)
144
148
  !match(*args).nil?
data/lib/httpx/io/unix.rb CHANGED
@@ -33,7 +33,7 @@ module HTTPX
33
33
  else
34
34
  if @options.transport_options
35
35
  # :nocov:
36
- warn ":#{__method__} is deprecated, use :addresses instead"
36
+ warn ":transport_options is deprecated, use :addresses instead"
37
37
  @path = @options.transport_options[:path]
38
38
  # :nocov:
39
39
  else
data/lib/httpx/options.rb CHANGED
@@ -10,6 +10,7 @@ module HTTPX
10
10
  OPERATION_TIMEOUT = 60
11
11
  KEEP_ALIVE_TIMEOUT = 20
12
12
  SETTINGS_TIMEOUT = 10
13
+ READ_TIMEOUT = WRITE_TIMEOUT = REQUEST_TIMEOUT = Float::INFINITY
13
14
 
14
15
  # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
15
16
  ip_address_families = begin
@@ -34,6 +35,9 @@ module HTTPX
34
35
  settings_timeout: SETTINGS_TIMEOUT,
35
36
  operation_timeout: OPERATION_TIMEOUT,
36
37
  keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
38
+ read_timeout: READ_TIMEOUT,
39
+ write_timeout: WRITE_TIMEOUT,
40
+ request_timeout: REQUEST_TIMEOUT,
37
41
  },
38
42
  :headers => {},
39
43
  :window_size => WINDOW_SIZE,
@@ -197,7 +201,7 @@ module HTTPX
197
201
  end
198
202
 
199
203
  %i[
200
- params form json body ssl http2_settings
204
+ params form json xml body ssl http2_settings
201
205
  request_class response_class headers_class request_body_class
202
206
  response_body_class connection_class options_class
203
207
  io fallback_protocol debug debug_level transport_options resolver_class resolver_options
@@ -206,7 +210,7 @@ module HTTPX
206
210
  def_option(method_name)
207
211
  end
208
212
 
209
- REQUEST_IVARS = %i[@params @form @json @body].freeze
213
+ REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
210
214
  private_constant :REQUEST_IVARS
211
215
 
212
216
  def ==(other)
@@ -263,7 +267,7 @@ module HTTPX
263
267
  instance_variables.each do |ivar|
264
268
  value = other.instance_variable_get(ivar)
265
269
  value = case value
266
- when Symbol, Fixnum, TrueClass, FalseClass # rubocop:disable Lint/UnifiedInteger
270
+ when Symbol, Numeric, TrueClass, FalseClass
267
271
  value
268
272
  else
269
273
  value.dup
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins::CircuitBreaker
5
+ #
6
+ # A circuit is assigned to a given absoolute url or origin.
7
+ #
8
+ # It sets +max_attempts+, the number of attempts the circuit allows, before it is opened.
9
+ # It sets +reset_attempts_in+, the time a circuit stays open at most, before it resets.
10
+ # It sets +break_in+, the time that must elapse before an open circuit can transit to the half-open state.
11
+ # It sets +circuit_breaker_half_open_drip_rate+, the rate of requests a circuit allows to be performed when in an half-open state.
12
+ #
13
+ class Circuit
14
+ def initialize(max_attempts, reset_attempts_in, break_in, circuit_breaker_half_open_drip_rate)
15
+ @max_attempts = max_attempts
16
+ @reset_attempts_in = reset_attempts_in
17
+ @break_in = break_in
18
+ @circuit_breaker_half_open_drip_rate = 1 - circuit_breaker_half_open_drip_rate
19
+ @attempts = 0
20
+ @state = :closed
21
+ end
22
+
23
+ def respond
24
+ try_close
25
+
26
+ case @state
27
+ when :closed
28
+ nil
29
+ when :half_open
30
+ # return nothing or smth based on ratio
31
+ return if Random.rand >= @circuit_breaker_half_open_drip_rate
32
+
33
+ @response
34
+ when :open
35
+
36
+ @response
37
+ end
38
+ end
39
+
40
+ def try_open(response)
41
+ return unless @state == :closed
42
+
43
+ now = Utils.now
44
+
45
+ if @attempts.positive?
46
+ @attempts = 0 if now - @attempted_at > @reset_attempts_in
47
+ else
48
+ @attempted_at = now
49
+ end
50
+
51
+ @attempts += 1
52
+
53
+ return unless @attempts >= @max_attempts
54
+
55
+ @state = :open
56
+ @opened_at = now
57
+ @response = response
58
+ end
59
+
60
+ def try_close
61
+ case @state
62
+ when :closed
63
+ nil
64
+ when :half_open
65
+ # reset!
66
+ @attempts = 0
67
+ @opened_at = @attempted_at = @response = nil
68
+ @state = :closed
69
+
70
+ when :open
71
+ @state = :half_open if Utils.elapsed_time(@opened_at) > @break_in
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX::Plugins::CircuitBreaker
4
+ using HTTPX::URIExtensions
5
+
6
+ class CircuitStore
7
+ def initialize(options)
8
+ @circuits = Hash.new do |h, k|
9
+ h[k] = Circuit.new(
10
+ options.circuit_breaker_max_attempts,
11
+ options.circuit_breaker_reset_attempts_in,
12
+ options.circuit_breaker_break_in,
13
+ options.circuit_breaker_half_open_drip_rate
14
+ )
15
+ end
16
+ end
17
+
18
+ def try_open(uri, response)
19
+ circuit = get_circuit_for_uri(uri)
20
+
21
+ circuit.try_open(response)
22
+ end
23
+
24
+ # if circuit is open, it'll respond with the stored response.
25
+ # if not, nil.
26
+ def try_respond(request)
27
+ circuit = get_circuit_for_uri(request.uri)
28
+
29
+ circuit.respond
30
+ end
31
+
32
+ private
33
+
34
+ def get_circuit_for_uri(uri)
35
+ uri = URI(uri)
36
+
37
+ if @circuits.key?(uri.origin)
38
+ @circuits[uri.origin]
39
+ else
40
+ @circuits[uri.to_s]
41
+ end
42
+ end
43
+ end
44
+ end