httpx 0.20.3 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/release_notes/0_13_0.md +1 -1
- data/doc/release_notes/0_20_4.md +17 -0
- data/doc/release_notes/0_20_5.md +3 -0
- data/doc/release_notes/0_21_0.md +94 -0
- data/lib/httpx/connection/http1.rb +2 -1
- data/lib/httpx/connection.rb +41 -2
- data/lib/httpx/errors.rb +18 -0
- data/lib/httpx/extensions.rb +36 -13
- 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/compression.rb +1 -1
- 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/response_cache/store.rb +39 -19
- data/lib/httpx/plugins/response_cache.rb +98 -8
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +78 -0
- data/lib/httpx/pool.rb +1 -1
- data/lib/httpx/request.rb +15 -25
- data/lib/httpx/resolver/https.rb +2 -7
- data/lib/httpx/resolver/multi.rb +1 -1
- data/lib/httpx/resolver/native.rb +2 -1
- data/lib/httpx/resolver/resolver.rb +12 -2
- data/lib/httpx/response.rb +30 -9
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/transcoder/body.rb +1 -1
- 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/plugins/response_cache.rbs +24 -4
- data/sig/registry.rbs +5 -4
- data/sig/request.rbs +7 -1
- data/sig/resolver/native.rbs +5 -2
- data/sig/response.rbs +6 -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 +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c62cb1d027ee7e62770de459b00411614372578117ea56354df0d9114cf4397b
|
4
|
+
data.tar.gz: 18275725fc8adeac596f02f83f034dfcfe24a7e4c96ec34bc11400e8ca0e9567
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 840bf59a4cbbb45d26a836ccd4060007470d380314b4aedea58e49cb434b050a8252f7db3823daf9c2dfdb5d54075215e4ee1216c80852afbb7abfd57e1dfea2
|
7
|
+
data.tar.gz: 35a666b92236240e8ccd09b1dc29a2ccf83061222b0511d35eee6fb7f3794dc737ca86ac9ca7d87ba01e03d8e648b07e6e52290b02b25674bd783b7f9d2cfbce
|
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,17 @@
|
|
1
|
+
# 0.20.4
|
2
|
+
|
3
|
+
## Improvements
|
4
|
+
|
5
|
+
The `:response_cache` plugin is now more compliant with how the RFC 2616 defines which behaviour caches shall have:
|
6
|
+
|
7
|
+
* it caches only responses with one of the following status codes: 200, 203, 300, 301, 410.
|
8
|
+
* it discards cached responses which become stale.
|
9
|
+
* it supports "cache-control" header directives to decided when to cache, to store, what the response "age" is.
|
10
|
+
* it can cache more than one response for the same request, provided that the request presents different header values for the headers declared in the "vary" response header (previously, it was only caching the first response, and discarding the remainder).
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
## Bugfixes
|
15
|
+
|
16
|
+
* fixed DNS resolution bug which caused a loop when a failed connection attempt would cause a new DNS request to be triggered for the same domain, filling up and giving preference to the very IP which failed the attempt.
|
17
|
+
* response_cache: request verb is now taken into account, not causing HEAD/GET confusion for the same URL.
|
@@ -0,0 +1,94 @@
|
|
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 aa 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
|
+
```ruby
|
30
|
+
http = HTTPX.plugin(:circuit_breaker)
|
31
|
+
# that's it!
|
32
|
+
http.get(...
|
33
|
+
```
|
34
|
+
|
35
|
+
### WebDAV plugin
|
36
|
+
|
37
|
+
https://gitlab.com/honeyryderchuck/httpx/-/wikis/WebDav
|
38
|
+
|
39
|
+
The `:webdav` introduces some "convenience" methods to perform common WebDAV operations.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
webdav = HTTPX.plugin(:webdav, origin: "http://webdav-server")
|
43
|
+
.plugin(:digest_authentication).digest_auth("user", "pass")
|
44
|
+
|
45
|
+
res = webdav.put("/file.html", body: "this is the file body")
|
46
|
+
res = webdav.copy("/file.html", "/newdir/copy.html")
|
47
|
+
# ...
|
48
|
+
```
|
49
|
+
|
50
|
+
### XML transcoder, `:xml` option and `response.xml`
|
51
|
+
|
52
|
+
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:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
response = HTTPX.post("https://xml-server.com", xml: Nokogiri::XML("<xml ..."))
|
56
|
+
response.xml #=> #(Document:0x16e4 { name = "document", children = ...
|
57
|
+
```
|
58
|
+
|
59
|
+
## Improvements
|
60
|
+
|
61
|
+
### `:proxy` plugin: `:no_proxy` option
|
62
|
+
|
63
|
+
Support was added, in the `:proxy` plugin, to declare domains, either via regexp patterns, or strings, for which requests should bypass the proxy.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
http = HTTPX.plugin(:proxy).with_proxy(
|
67
|
+
uri: "http://10.10.0.1:51432",
|
68
|
+
no_proxy: ["gitlab.local", /*.google.com/]
|
69
|
+
)
|
70
|
+
http.get("https://duckduckgo.com/?q=httpx") #=> proxied
|
71
|
+
http.get("https://google.com/?q=httpx") #=> not proxied
|
72
|
+
http.get("https://gitlab.com") #=> proxied
|
73
|
+
http.get("https://gitlab.local") #=> not proxied
|
74
|
+
```
|
75
|
+
|
76
|
+
### OOTB support for other JSON libraries
|
77
|
+
|
78
|
+
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).
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require "oj"
|
82
|
+
require "httpx"
|
83
|
+
|
84
|
+
response = HTTPX.post("https://somedomain.json", json: { "foo" => "bar" }) # will use "oj"
|
85
|
+
puts response.json # will use "oj"
|
86
|
+
```
|
87
|
+
|
88
|
+
## Bugfixes
|
89
|
+
|
90
|
+
* `:expect` plugin: `:expect_timeout` can accept floats (not just integers).
|
91
|
+
|
92
|
+
## Chore
|
93
|
+
|
94
|
+
* 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").
|
@@ -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)
|
@@ -573,7 +577,7 @@ module HTTPX
|
|
573
577
|
error = ex
|
574
578
|
else
|
575
579
|
# inactive connections do not contribute to the select loop, therefore
|
576
|
-
# they should fail due to such errors.
|
580
|
+
# they should not fail due to such errors.
|
577
581
|
return if @state == :inactive
|
578
582
|
|
579
583
|
if @timeout
|
@@ -591,10 +595,45 @@ module HTTPX
|
|
591
595
|
def handle_error(error)
|
592
596
|
parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
|
593
597
|
while (request = @pending.shift)
|
594
|
-
response = ErrorResponse.new(request, error,
|
598
|
+
response = ErrorResponse.new(request, error, request.options)
|
595
599
|
request.response = response
|
596
600
|
request.emit(:response, response)
|
597
601
|
end
|
598
602
|
end
|
603
|
+
|
604
|
+
def set_request_timeouts(request)
|
605
|
+
write_timeout = request.write_timeout
|
606
|
+
request.once(:headers) do
|
607
|
+
@timers.after(write_timeout) { write_timeout_callback(request, write_timeout) }
|
608
|
+
end unless write_timeout.nil? || write_timeout.infinite?
|
609
|
+
|
610
|
+
read_timeout = request.read_timeout
|
611
|
+
request.once(:done) do
|
612
|
+
@timers.after(read_timeout) { read_timeout_callback(request, read_timeout) }
|
613
|
+
end unless read_timeout.nil? || read_timeout.infinite?
|
614
|
+
|
615
|
+
request_timeout = request.request_timeout
|
616
|
+
request.once(:headers) do
|
617
|
+
@timers.after(request_timeout) { read_timeout_callback(request, request_timeout, RequestTimeoutError) }
|
618
|
+
end unless request_timeout.nil? || request_timeout.infinite?
|
619
|
+
end
|
620
|
+
|
621
|
+
def write_timeout_callback(request, write_timeout)
|
622
|
+
return if request.state == :done
|
623
|
+
|
624
|
+
@write_buffer.clear
|
625
|
+
error = WriteTimeoutError.new(request, nil, write_timeout)
|
626
|
+
on_error(error)
|
627
|
+
end
|
628
|
+
|
629
|
+
def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
|
630
|
+
response = request.response
|
631
|
+
|
632
|
+
return if response && response.finished?
|
633
|
+
|
634
|
+
@write_buffer.clear
|
635
|
+
error = error_type.new(request, request.response, read_timeout)
|
636
|
+
on_error(error)
|
637
|
+
end
|
599
638
|
end
|
600
639
|
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)
|
@@ -83,22 +91,41 @@ module HTTPX
|
|
83
91
|
end
|
84
92
|
|
85
93
|
module ArrayExtensions
|
86
|
-
|
94
|
+
module FilterMap
|
95
|
+
refine Array do
|
87
96
|
|
88
|
-
|
89
|
-
|
97
|
+
def filter_map
|
98
|
+
return to_enum(:filter_map) unless block_given?
|
90
99
|
|
91
|
-
|
92
|
-
|
93
|
-
|
100
|
+
each_with_object([]) do |item, res|
|
101
|
+
processed = yield(item)
|
102
|
+
res << processed if processed
|
103
|
+
end
|
94
104
|
end
|
95
105
|
end unless Array.method_defined?(:filter_map)
|
106
|
+
end
|
96
107
|
|
97
|
-
|
98
|
-
|
99
|
-
|
108
|
+
module Sum
|
109
|
+
refine Array do
|
110
|
+
def sum(accumulator = 0, &block)
|
111
|
+
values = block_given? ? map(&block) : self
|
112
|
+
values.inject(accumulator, :+)
|
113
|
+
end
|
100
114
|
end unless Array.method_defined?(:sum)
|
101
115
|
end
|
116
|
+
|
117
|
+
module Intersect
|
118
|
+
refine Array do
|
119
|
+
def intersect?(arr)
|
120
|
+
if size < arr.size
|
121
|
+
smaller = self
|
122
|
+
else
|
123
|
+
smaller, arr = arr, self
|
124
|
+
end
|
125
|
+
(arr & smaller).size > 0
|
126
|
+
end
|
127
|
+
end unless Array.method_defined?(:intersect?)
|
128
|
+
end
|
102
129
|
end
|
103
130
|
|
104
131
|
module IOExtensions
|
@@ -116,10 +143,6 @@ module HTTPX
|
|
116
143
|
end
|
117
144
|
|
118
145
|
module RegexpExtensions
|
119
|
-
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
120
|
-
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
121
|
-
# monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
|
122
|
-
# :nocov:
|
123
146
|
refine(Regexp) do
|
124
147
|
def match?(*args)
|
125
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::DEFAULT.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
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin implements a circuit breaker around connection errors.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/Circuit-Breaker
|
9
|
+
#
|
10
|
+
module CircuitBreaker
|
11
|
+
using URIExtensions
|
12
|
+
|
13
|
+
def self.load_dependencies(*)
|
14
|
+
require_relative "circuit_breaker/circuit"
|
15
|
+
require_relative "circuit_breaker/circuit_store"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.extra_options(options)
|
19
|
+
options.merge(circuit_breaker_max_attempts: 3, circuit_breaker_reset_attempts_in: 60, circuit_breaker_break_in: 60,
|
20
|
+
circuit_breaker_half_open_drip_rate: 1)
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
def initialize(*)
|
25
|
+
super
|
26
|
+
@circuit_store = CircuitStore.new(@options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize_dup(orig)
|
30
|
+
super
|
31
|
+
@circuit_store = orig.instance_variable_get(:@circuit_store).dup
|
32
|
+
end
|
33
|
+
|
34
|
+
def send_requests(*requests)
|
35
|
+
# @type var short_circuit_responses: Array[response]
|
36
|
+
short_circuit_responses = []
|
37
|
+
|
38
|
+
# run all requests through the circuit breaker, see if the circuit is
|
39
|
+
# open for any of them.
|
40
|
+
real_requests = requests.each_with_object([]) do |req, real_reqs|
|
41
|
+
short_circuit_response = @circuit_store.try_respond(req)
|
42
|
+
if short_circuit_response.nil?
|
43
|
+
real_reqs << req
|
44
|
+
next
|
45
|
+
end
|
46
|
+
short_circuit_responses[requests.index(req)] = short_circuit_response
|
47
|
+
end
|
48
|
+
|
49
|
+
# run requests for the remainder
|
50
|
+
unless real_requests.empty?
|
51
|
+
responses = super(*real_requests)
|
52
|
+
|
53
|
+
real_requests.each_with_index do |request, idx|
|
54
|
+
short_circuit_responses[requests.index(request)] = responses[idx]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
short_circuit_responses
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_response(request, response)
|
62
|
+
if response.is_a?(ErrorResponse)
|
63
|
+
case response.error
|
64
|
+
when RequestTimeoutError
|
65
|
+
@circuit_store.try_open(request.uri, response)
|
66
|
+
else
|
67
|
+
@circuit_store.try_open(request.origin, response)
|
68
|
+
end
|
69
|
+
elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
|
70
|
+
@circuit_store.try_open(request.uri, response)
|
71
|
+
end
|
72
|
+
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module OptionsMethods
|
78
|
+
def option_circuit_breaker_max_attempts(value)
|
79
|
+
attempts = Integer(value)
|
80
|
+
raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
|
81
|
+
|
82
|
+
attempts
|
83
|
+
end
|
84
|
+
|
85
|
+
def option_circuit_breaker_reset_attempts_in(value)
|
86
|
+
timeout = Float(value)
|
87
|
+
raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
|
88
|
+
|
89
|
+
timeout
|
90
|
+
end
|
91
|
+
|
92
|
+
def option_circuit_breaker_break_in(value)
|
93
|
+
timeout = Float(value)
|
94
|
+
raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
|
95
|
+
|
96
|
+
timeout
|
97
|
+
end
|
98
|
+
|
99
|
+
def option_circuit_breaker_half_open_drip_rate(value)
|
100
|
+
ratio = Float(value)
|
101
|
+
raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
|
102
|
+
|
103
|
+
ratio
|
104
|
+
end
|
105
|
+
|
106
|
+
def option_circuit_breaker_break_on(value)
|
107
|
+
raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
|
108
|
+
|
109
|
+
value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
register_plugin :circuit_breaker, CircuitBreaker
|
114
|
+
end
|
115
|
+
end
|
@@ -42,7 +42,7 @@ module HTTPX
|
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
-
def on_response(
|
45
|
+
def on_response(_request, response)
|
46
46
|
if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
|
47
47
|
|
48
48
|
log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
|
data/lib/httpx/plugins/expect.rb
CHANGED