httpx 0.20.5 → 0.21.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +44 -26
- data/doc/release_notes/0_13_0.md +1 -1
- data/doc/release_notes/0_21_0.md +96 -0
- data/doc/release_notes/0_21_1.md +12 -0
- data/lib/httpx/connection/http1.rb +2 -1
- data/lib/httpx/connection.rb +47 -3
- data/lib/httpx/errors.rb +18 -0
- data/lib/httpx/extensions.rb +8 -4
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/options.rb +7 -3
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
- data/lib/httpx/plugins/circuit_breaker.rb +115 -0
- data/lib/httpx/plugins/cookies.rb +1 -1
- data/lib/httpx/plugins/expect.rb +1 -1
- data/lib/httpx/plugins/multipart/decoder.rb +1 -1
- data/lib/httpx/plugins/proxy.rb +7 -1
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +78 -0
- data/lib/httpx/request.rb +15 -25
- data/lib/httpx/resolver/https.rb +2 -7
- data/lib/httpx/resolver/native.rb +19 -8
- data/lib/httpx/response.rb +27 -9
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/transcoder/form.rb +1 -1
- data/lib/httpx/transcoder/json.rb +19 -3
- data/lib/httpx/transcoder/xml.rb +57 -0
- data/lib/httpx/transcoder.rb +1 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +1 -1
- data/sig/chainable.rbs +1 -0
- data/sig/connection.rbs +12 -4
- data/sig/errors.rbs +13 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +4 -1
- data/sig/plugins/circuit_breaker.rbs +61 -0
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +3 -3
- data/sig/plugins/compression.rbs +1 -1
- data/sig/plugins/multipart.rbs +1 -1
- data/sig/plugins/proxy/socks5.rbs +3 -2
- data/sig/plugins/proxy.rbs +1 -1
- data/sig/registry.rbs +5 -4
- data/sig/request.rbs +7 -1
- data/sig/resolver/native.rbs +5 -2
- data/sig/response.rbs +3 -1
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/json.rbs +4 -1
- data/sig/transcoder/xml.rbs +21 -0
- data/sig/transcoder.rbs +2 -2
- data/sig/utils.rbs +2 -2
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1195773336c2fb0b92ed10abfafa8a99c7cfd19af31b932dbfff72d073ed3f47
|
4
|
+
data.tar.gz: 634e9e62edcd8184d014b1870cbc303cdfcc8035e0c76431b10c1e9143bdbb23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
25
|
+
* Advanced Cookie handling
|
26
26
|
* HTTP/2 Server Push
|
27
|
-
*
|
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
|
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
|
-
##
|
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
|
-
|
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
|
-
|
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",
|
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
|
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
|
-
|
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
|
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
|
-
##
|
131
|
+
## User-driven test suite
|
111
132
|
|
112
|
-
The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/httpbin/), so
|
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.
|
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
|
-
|
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
|
|
data/doc/release_notes/0_13_0.md
CHANGED
@@ -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.
|
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?
|
data/lib/httpx/connection.rb
CHANGED
@@ -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::
|
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,
|
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
|
data/lib/httpx/extensions.rb
CHANGED
@@ -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 "
|
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,
|
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
|