httpx 0.20.5 → 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_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 +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 +2 -1
- 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 +12 -2
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,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)
|
@@ -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::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
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -105,6 +105,10 @@ module HTTPX
|
|
105
105
|
end
|
106
106
|
return if @_proxy_uris.empty?
|
107
107
|
|
108
|
+
proxy = options.proxy
|
109
|
+
|
110
|
+
return { uri: uri.host } if proxy && proxy.key?(:no_proxy) && !Array(proxy[:no_proxy]).grep(uri.host).empty?
|
111
|
+
|
108
112
|
proxy_opts = { uri: @_proxy_uris.first }
|
109
113
|
proxy_opts = options.proxy.merge(proxy_opts) if options.proxy
|
110
114
|
proxy_opts
|
@@ -117,7 +121,9 @@ module HTTPX
|
|
117
121
|
next_proxy = proxy_uris(uri, options)
|
118
122
|
raise Error, "Failed to connect to proxy" unless next_proxy
|
119
123
|
|
120
|
-
|
124
|
+
proxy = Parameters.new(**next_proxy) unless next_proxy[:uri] == uri.host
|
125
|
+
|
126
|
+
proxy_options = options.merge(proxy: proxy)
|
121
127
|
connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
|
122
128
|
unless connections.nil? || connections.include?(connection)
|
123
129
|
connections << connection
|