httpx 0.24.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +0 -48
- data/README.md +4 -13
- data/doc/release_notes/0_24_4.md +3 -3
- data/doc/release_notes/0_24_6.md +5 -0
- data/doc/release_notes/1_0_0.md +60 -0
- data/lib/httpx/adapters/datadog.rb +28 -97
- data/lib/httpx/adapters/faraday.rb +9 -52
- data/lib/httpx/adapters/webmock.rb +2 -7
- data/lib/httpx/altsvc.rb +4 -22
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/chainable.rb +2 -25
- data/lib/httpx/connection.rb +11 -32
- data/lib/httpx/domain_name.rb +5 -12
- data/lib/httpx/errors.rb +0 -2
- data/lib/httpx/extensions.rb +0 -124
- data/lib/httpx/io/ssl.rb +26 -59
- data/lib/httpx/io/tcp.rb +27 -68
- data/lib/httpx/io/udp.rb +13 -48
- data/lib/httpx/io/unix.rb +1 -8
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +24 -84
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +2 -5
- data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
- data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
- data/lib/httpx/plugins/auth.rb +25 -0
- data/lib/httpx/plugins/aws_sigv4.rb +0 -1
- data/lib/httpx/plugins/{basic_authentication.rb → basic_auth.rb} +5 -6
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +40 -16
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +16 -5
- data/lib/httpx/plugins/circuit_breaker.rb +11 -4
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +1 -1
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +5 -5
- data/lib/httpx/plugins/follow_redirects.rb +21 -24
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +82 -0
- data/lib/httpx/plugins/grpc/message.rb +7 -39
- data/lib/httpx/plugins/grpc.rb +15 -21
- data/lib/httpx/plugins/h2c.rb +0 -1
- data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +5 -5
- data/lib/httpx/plugins/oauth.rb +2 -2
- data/lib/httpx/plugins/proxy/http.rb +0 -2
- data/lib/httpx/plugins/proxy/socks4.rb +0 -4
- data/lib/httpx/plugins/proxy/socks5.rb +1 -5
- data/lib/httpx/plugins/proxy.rb +3 -32
- data/lib/httpx/plugins/retries.rb +3 -4
- data/lib/httpx/plugins/stream.rb +4 -6
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +145 -0
- data/lib/httpx/request.rb +2 -119
- data/lib/httpx/resolver/https.rb +1 -1
- data/lib/httpx/resolver/native.rb +6 -14
- data/lib/httpx/resolver/resolver.rb +1 -1
- data/lib/httpx/resolver/system.rb +11 -9
- data/lib/httpx/response/body.rb +206 -0
- data/lib/httpx/response/buffer.rb +90 -0
- data/lib/httpx/response.rb +5 -208
- data/lib/httpx/selector.rb +0 -2
- data/lib/httpx/session.rb +3 -10
- data/lib/httpx/transcoder/body.rb +0 -1
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +52 -32
- data/lib/httpx/transcoder/gzip.rb +74 -0
- data/lib/httpx/transcoder/json.rb +2 -4
- data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
- data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +3 -3
- data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
- data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
- data/lib/httpx/transcoder/multipart.rb +17 -0
- data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
- data/lib/httpx/transcoder/utils/deflater.rb +72 -0
- data/lib/httpx/transcoder/xml.rb +0 -2
- data/lib/httpx/transcoder.rb +2 -2
- data/lib/httpx/utils.rb +10 -20
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +0 -8
- data/sig/chainable.rbs +5 -6
- data/sig/connection.rbs +0 -1
- data/sig/errors.rbs +0 -3
- data/sig/httpx.rbs +2 -1
- data/sig/io/unix.rbs +1 -1
- data/sig/options.rbs +12 -8
- data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
- data/sig/plugins/auth.rbs +13 -0
- data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
- data/sig/plugins/brotli.rbs +22 -0
- data/sig/plugins/circuit_breaker.rbs +7 -3
- data/sig/plugins/compression.rbs +0 -2
- data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
- data/sig/plugins/follow_redirects.rbs +0 -1
- data/sig/plugins/grpc/call.rbs +19 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +33 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +2 -32
- data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
- data/sig/plugins/oauth.rbs +1 -1
- data/sig/plugins/proxy/socks4.rbs +2 -3
- data/sig/plugins/proxy/socks5.rbs +0 -1
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/response_cache.rbs +5 -2
- data/sig/request/body.rbs +42 -0
- data/sig/request.rbs +1 -27
- data/sig/resolver/resolver.rbs +1 -1
- data/sig/response/body.rbs +52 -0
- data/sig/response/buffer.rbs +24 -0
- data/sig/response.rbs +0 -39
- data/sig/session.rbs +2 -0
- data/sig/transcoder/body.rbs +4 -3
- data/sig/transcoder/deflate.rbs +11 -0
- data/sig/transcoder/form.rbs +5 -3
- data/sig/transcoder/gzip.rbs +24 -0
- data/sig/transcoder/json.rbs +4 -2
- data/sig/{plugins → transcoder}/multipart.rbs +3 -10
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +29 -0
- data/sig/transcoder.rbs +18 -2
- metadata +52 -34
- data/lib/httpx/plugins/authentication.rb +0 -24
- data/lib/httpx/plugins/compression/brotli.rb +0 -54
- data/lib/httpx/plugins/compression/deflate.rb +0 -54
- data/lib/httpx/plugins/compression/gzip.rb +0 -90
- data/lib/httpx/plugins/compression.rb +0 -165
- data/lib/httpx/plugins/multipart/decoder.rb +0 -137
- data/lib/httpx/plugins/multipart.rb +0 -96
- data/sig/plugins/authentication.rbs +0 -13
- data/sig/plugins/compression/brotli.rbs +0 -21
- data/sig/plugins/compression/deflate.rbs +0 -17
- data/sig/plugins/compression/gzip.rbs +0 -29
- /data/sig/plugins/{authentication → auth}/digest.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
data/lib/httpx/options.rb
CHANGED
@@ -7,11 +7,10 @@ module HTTPX
|
|
7
7
|
BUFFER_SIZE = 1 << 14
|
8
8
|
WINDOW_SIZE = 1 << 14 # 16K
|
9
9
|
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
|
10
|
-
CONNECT_TIMEOUT = 60
|
11
|
-
OPERATION_TIMEOUT = 60
|
12
10
|
KEEP_ALIVE_TIMEOUT = 20
|
13
11
|
SETTINGS_TIMEOUT = 10
|
14
|
-
|
12
|
+
CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
|
13
|
+
REQUEST_TIMEOUT = OPERATION_TIMEOUT = Float::INFINITY
|
15
14
|
|
16
15
|
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
17
16
|
ip_address_families = begin
|
@@ -31,6 +30,9 @@ module HTTPX
|
|
31
30
|
:ssl => {},
|
32
31
|
:http2_settings => { settings_enable_push: 0 },
|
33
32
|
:fallback_protocol => "http/1.1",
|
33
|
+
:supported_compression_formats => %w[gzip deflate],
|
34
|
+
:decompress_response_body => true,
|
35
|
+
:compress_request_body => true,
|
34
36
|
:timeout => {
|
35
37
|
connect_timeout: CONNECT_TIMEOUT,
|
36
38
|
settings_timeout: SETTINGS_TIMEOUT,
|
@@ -52,7 +54,6 @@ module HTTPX
|
|
52
54
|
:connection_class => Class.new(Connection),
|
53
55
|
:options_class => Class.new(self),
|
54
56
|
:transport => nil,
|
55
|
-
:transport_options => nil,
|
56
57
|
:addresses => nil,
|
57
58
|
:persistent => false,
|
58
59
|
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
@@ -60,28 +61,6 @@ module HTTPX
|
|
60
61
|
:ip_families => ip_address_families,
|
61
62
|
}.freeze
|
62
63
|
|
63
|
-
begin
|
64
|
-
module HashExtensions
|
65
|
-
refine Hash do
|
66
|
-
def >=(other)
|
67
|
-
Hash[other] <= self
|
68
|
-
end
|
69
|
-
|
70
|
-
def <=(other)
|
71
|
-
other = Hash[other]
|
72
|
-
return false unless size <= other.size
|
73
|
-
|
74
|
-
each do |k, v|
|
75
|
-
v2 = other.fetch(k) { return false }
|
76
|
-
return false unless v2 == v
|
77
|
-
end
|
78
|
-
true
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
using HashExtensions
|
83
|
-
end unless Hash.method_defined?(:>=)
|
84
|
-
|
85
64
|
class << self
|
86
65
|
def new(options = {})
|
87
66
|
# let enhanced options go through
|
@@ -100,38 +79,10 @@ module HTTPX
|
|
100
79
|
|
101
80
|
attr_reader(optname)
|
102
81
|
end
|
103
|
-
|
104
|
-
def def_option(optname, *args, &block)
|
105
|
-
if args.empty? && !block
|
106
|
-
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
107
|
-
def option_#{optname}(v); v; end # def option_smth(v); v; end
|
108
|
-
OUT
|
109
|
-
return
|
110
|
-
end
|
111
|
-
|
112
|
-
deprecated_def_option(optname, *args, &block)
|
113
|
-
end
|
114
|
-
|
115
|
-
def deprecated_def_option(optname, layout = nil, &interpreter)
|
116
|
-
warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
|
117
|
-
"Define module OptionsMethods and `def option_#{optname}(val)` instead."
|
118
|
-
|
119
|
-
if layout
|
120
|
-
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
121
|
-
def option_#{optname}(value) # def option_origin(v)
|
122
|
-
#{layout} # URI(v)
|
123
|
-
end # end
|
124
|
-
OUT
|
125
|
-
elsif interpreter
|
126
|
-
define_method(:"option_#{optname}") do |value|
|
127
|
-
instance_exec(value, &interpreter)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
82
|
end
|
132
83
|
|
133
84
|
def initialize(options = {})
|
134
|
-
|
85
|
+
do_initialize(options)
|
135
86
|
freeze
|
136
87
|
end
|
137
88
|
|
@@ -142,6 +93,7 @@ module HTTPX
|
|
142
93
|
@timeout.freeze
|
143
94
|
@headers.freeze
|
144
95
|
@addresses.freeze
|
96
|
+
@supported_compression_formats.freeze
|
145
97
|
end
|
146
98
|
|
147
99
|
def option_origin(value)
|
@@ -157,14 +109,11 @@ module HTTPX
|
|
157
109
|
end
|
158
110
|
|
159
111
|
def option_timeout(value)
|
160
|
-
|
161
|
-
|
162
|
-
if timeouts.key?(:loop_timeout)
|
163
|
-
warn ":loop_timeout is deprecated, use :operation_timeout instead"
|
164
|
-
timeouts[:operation_timeout] = timeouts.delete(:loop_timeout)
|
165
|
-
end
|
112
|
+
Hash[value]
|
113
|
+
end
|
166
114
|
|
167
|
-
|
115
|
+
def option_supported_compression_formats(value)
|
116
|
+
Array(value).map(&:to_s)
|
168
117
|
end
|
169
118
|
|
170
119
|
def option_max_concurrent_requests(value)
|
@@ -196,7 +145,10 @@ module HTTPX
|
|
196
145
|
end
|
197
146
|
|
198
147
|
def option_body_threshold_size(value)
|
199
|
-
Integer(value)
|
148
|
+
bytes = Integer(value)
|
149
|
+
raise TypeError, ":body_threshold_size must be positive" unless bytes.positive?
|
150
|
+
|
151
|
+
bytes
|
200
152
|
end
|
201
153
|
|
202
154
|
def option_transport(value)
|
@@ -218,10 +170,13 @@ module HTTPX
|
|
218
170
|
params form json xml body ssl http2_settings
|
219
171
|
request_class response_class headers_class request_body_class
|
220
172
|
response_body_class connection_class options_class
|
221
|
-
io fallback_protocol debug debug_level
|
173
|
+
io fallback_protocol debug debug_level resolver_class resolver_options
|
174
|
+
compress_request_body decompress_response_body
|
222
175
|
persistent
|
223
176
|
].each do |method_name|
|
224
|
-
|
177
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
178
|
+
def option_#{method_name}(v); v; end # def option_smth(v); v; end
|
179
|
+
OUT
|
225
180
|
end
|
226
181
|
|
227
182
|
REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
|
@@ -270,30 +225,15 @@ module HTTPX
|
|
270
225
|
end
|
271
226
|
end
|
272
227
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
|
277
|
-
end
|
278
|
-
end
|
279
|
-
else
|
280
|
-
def initialize_dup(other)
|
281
|
-
instance_variables.each do |ivar|
|
282
|
-
value = other.instance_variable_get(ivar)
|
283
|
-
value = case value
|
284
|
-
when Symbol, Numeric, TrueClass, FalseClass
|
285
|
-
value
|
286
|
-
else
|
287
|
-
value.dup
|
288
|
-
end
|
289
|
-
instance_variable_set(ivar, value)
|
290
|
-
end
|
228
|
+
def initialize_dup(other)
|
229
|
+
instance_variables.each do |ivar|
|
230
|
+
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
|
291
231
|
end
|
292
232
|
end
|
293
233
|
|
294
234
|
private
|
295
235
|
|
296
|
-
def
|
236
|
+
def do_initialize(options = {})
|
297
237
|
defaults = DEFAULT_OPTIONS.merge(options)
|
298
238
|
defaults.each do |k, v|
|
299
239
|
next if v.nil?
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
3
|
+
require "httpx/base64"
|
4
4
|
|
5
5
|
module HTTPX
|
6
6
|
module Plugins
|
@@ -11,10 +11,6 @@ module HTTPX
|
|
11
11
|
@password = password
|
12
12
|
end
|
13
13
|
|
14
|
-
def can_authenticate?(authenticate)
|
15
|
-
authenticate && /Basic .*/.match?(authenticate)
|
16
|
-
end
|
17
|
-
|
18
14
|
def authenticate(*)
|
19
15
|
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
|
20
16
|
end
|
@@ -8,8 +8,6 @@ module HTTPX
|
|
8
8
|
module Plugins
|
9
9
|
module Authentication
|
10
10
|
class Digest
|
11
|
-
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
12
|
-
|
13
11
|
def initialize(user, password, hashed: false, **)
|
14
12
|
@user = user
|
15
13
|
@password = password
|
@@ -31,9 +29,8 @@ module HTTPX
|
|
31
29
|
# discard first token, it's Digest
|
32
30
|
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
33
31
|
|
34
|
-
params =
|
35
|
-
|
36
|
-
.map { |k, v| [k, v.delete("\"")] }]
|
32
|
+
params = auth_info.split(/ *, */)
|
33
|
+
.to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
|
37
34
|
nonce = params["nonce"]
|
38
35
|
nc = next_nonce
|
39
36
|
|
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
3
|
+
require "httpx/base64"
|
4
4
|
require "ntlm"
|
5
5
|
|
6
6
|
module HTTPX
|
7
7
|
module Plugins
|
8
8
|
module Authentication
|
9
9
|
class Ntlm
|
10
|
-
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
11
|
-
|
12
10
|
def initialize(user, password, domain: nil)
|
13
11
|
@user = user
|
14
12
|
@password = password
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds a shim +authorization+ method to the session, which will fill
|
7
|
+
# the HTTP Authorization header, and another, +bearer_auth+, which fill the "Bearer " prefix
|
8
|
+
# in its value.
|
9
|
+
#
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/Auth#authorization
|
11
|
+
#
|
12
|
+
module Auth
|
13
|
+
module InstanceMethods
|
14
|
+
def authorization(token)
|
15
|
+
with(headers: { "authorization" => token })
|
16
|
+
end
|
17
|
+
|
18
|
+
def bearer_auth(token)
|
19
|
+
authorization("Bearer #{token}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
register_plugin :auth, Auth
|
24
|
+
end
|
25
|
+
end
|
@@ -5,26 +5,25 @@ module HTTPX
|
|
5
5
|
#
|
6
6
|
# This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
|
7
7
|
#
|
8
|
-
# https://gitlab.com/os85/httpx/wikis/
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Authorization#basic-auth
|
9
9
|
#
|
10
10
|
module BasicAuth
|
11
11
|
class << self
|
12
12
|
def load_dependencies(_klass)
|
13
|
-
require_relative "
|
13
|
+
require_relative "auth/basic"
|
14
14
|
end
|
15
15
|
|
16
16
|
def configure(klass)
|
17
|
-
klass.plugin(:
|
17
|
+
klass.plugin(:auth)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
module InstanceMethods
|
22
22
|
def basic_auth(user, password)
|
23
|
-
|
23
|
+
authorization(Authentication::Basic.new(user, password).authenticate)
|
24
24
|
end
|
25
|
-
alias_method :basic_authentication, :basic_auth
|
26
25
|
end
|
27
26
|
end
|
28
|
-
register_plugin :
|
27
|
+
register_plugin :basic_auth, BasicAuth
|
29
28
|
end
|
30
29
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module Brotli
|
6
|
+
class Deflater < Transcoder::Deflater
|
7
|
+
def deflate(chunk)
|
8
|
+
return unless chunk
|
9
|
+
|
10
|
+
::Brotli.deflate(chunk)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module RequestBodyClassMethods
|
15
|
+
def initialize_deflater_body(body, encoding)
|
16
|
+
return Brotli.encode(body) if encoding == "br"
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ResponseBodyClassMethods
|
23
|
+
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
24
|
+
return Brotli.decode(response, **kwargs) if encoding == "br"
|
25
|
+
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module_function
|
31
|
+
|
32
|
+
def load_dependencies(*)
|
33
|
+
require "brotli"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.extra_options(options)
|
37
|
+
options.merge(supported_compression_formats: %w[br] + options.supported_compression_formats)
|
38
|
+
end
|
39
|
+
|
40
|
+
def encode(body)
|
41
|
+
Deflater.new(body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode(_response, **)
|
45
|
+
::Brotli.method(:inflate)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
register_plugin :brotli, Brotli
|
49
|
+
end
|
50
|
+
end
|
@@ -15,8 +15,11 @@ module HTTPX
|
|
15
15
|
@max_attempts = max_attempts
|
16
16
|
@reset_attempts_in = reset_attempts_in
|
17
17
|
@break_in = break_in
|
18
|
-
@circuit_breaker_half_open_drip_rate =
|
18
|
+
@circuit_breaker_half_open_drip_rate = circuit_breaker_half_open_drip_rate
|
19
19
|
@attempts = 0
|
20
|
+
|
21
|
+
total_real_attempts = @max_attempts * @circuit_breaker_half_open_drip_rate
|
22
|
+
@drip_factor = (@max_attempts / total_real_attempts).round
|
20
23
|
@state = :closed
|
21
24
|
end
|
22
25
|
|
@@ -27,8 +30,13 @@ module HTTPX
|
|
27
30
|
when :closed
|
28
31
|
nil
|
29
32
|
when :half_open
|
30
|
-
|
31
|
-
|
33
|
+
@attempts += 1
|
34
|
+
|
35
|
+
# do real requests while drip rate valid
|
36
|
+
if (@real_attempts % @drip_factor).zero?
|
37
|
+
@real_attempts += 1
|
38
|
+
return
|
39
|
+
end
|
32
40
|
|
33
41
|
@response
|
34
42
|
when :open
|
@@ -38,23 +46,31 @@ module HTTPX
|
|
38
46
|
end
|
39
47
|
|
40
48
|
def try_open(response)
|
41
|
-
|
49
|
+
case @state
|
50
|
+
when :closed
|
51
|
+
now = Utils.now
|
42
52
|
|
43
|
-
|
53
|
+
if @attempts.positive?
|
54
|
+
# reset if error happened long ago
|
55
|
+
@attempts = 0 if now - @attempted_at > @reset_attempts_in
|
56
|
+
else
|
57
|
+
@attempted_at = now
|
58
|
+
end
|
44
59
|
|
45
|
-
|
46
|
-
@attempts = 0 if now - @attempted_at > @reset_attempts_in
|
47
|
-
else
|
48
|
-
@attempted_at = now
|
49
|
-
end
|
60
|
+
@attempts += 1
|
50
61
|
|
51
|
-
|
62
|
+
return unless @attempts >= @max_attempts
|
52
63
|
|
53
|
-
|
64
|
+
@state = :open
|
65
|
+
@opened_at = now
|
66
|
+
@response = response
|
67
|
+
when :half_open
|
68
|
+
# open immediately
|
54
69
|
|
55
|
-
|
56
|
-
|
57
|
-
|
70
|
+
@state = :open
|
71
|
+
@attempted_at = @opened_at = Utils.now
|
72
|
+
@response = response
|
73
|
+
end
|
58
74
|
end
|
59
75
|
|
60
76
|
def try_close
|
@@ -62,13 +78,21 @@ module HTTPX
|
|
62
78
|
when :closed
|
63
79
|
nil
|
64
80
|
when :half_open
|
81
|
+
|
82
|
+
# do not close circuit unless attempts exhausted
|
83
|
+
return unless @attempts >= @max_attempts
|
84
|
+
|
65
85
|
# reset!
|
66
86
|
@attempts = 0
|
67
87
|
@opened_at = @attempted_at = @response = nil
|
68
88
|
@state = :closed
|
69
89
|
|
70
90
|
when :open
|
71
|
-
|
91
|
+
if Utils.elapsed_time(@opened_at) > @break_in
|
92
|
+
@state = :half_open
|
93
|
+
@attempts = 0
|
94
|
+
@real_attempts = 0
|
95
|
+
end
|
72
96
|
end
|
73
97
|
end
|
74
98
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "mutex_m"
|
4
|
+
|
3
5
|
module HTTPX::Plugins::CircuitBreaker
|
4
6
|
using HTTPX::URIExtensions
|
5
7
|
|
@@ -13,18 +15,29 @@ module HTTPX::Plugins::CircuitBreaker
|
|
13
15
|
options.circuit_breaker_half_open_drip_rate
|
14
16
|
)
|
15
17
|
end
|
18
|
+
@circuits.extend(Mutex_m)
|
16
19
|
end
|
17
20
|
|
18
21
|
def try_open(uri, response)
|
19
|
-
circuit = get_circuit_for_uri(uri)
|
22
|
+
circuit = @circuits.synchronize { get_circuit_for_uri(uri) }
|
20
23
|
|
21
24
|
circuit.try_open(response)
|
22
25
|
end
|
23
26
|
|
27
|
+
def try_close(uri)
|
28
|
+
circuit = @circuits.synchronize do
|
29
|
+
return unless @circuits.key?(uri.origin) || @circuits.key?(uri.to_s)
|
30
|
+
|
31
|
+
get_circuit_for_uri(uri)
|
32
|
+
end
|
33
|
+
|
34
|
+
circuit.try_close
|
35
|
+
end
|
36
|
+
|
24
37
|
# if circuit is open, it'll respond with the stored response.
|
25
38
|
# if not, nil.
|
26
39
|
def try_respond(request)
|
27
|
-
circuit = get_circuit_for_uri(request.uri)
|
40
|
+
circuit = @circuits.synchronize { get_circuit_for_uri(request.uri) }
|
28
41
|
|
29
42
|
circuit.respond
|
30
43
|
end
|
@@ -32,9 +45,7 @@ module HTTPX::Plugins::CircuitBreaker
|
|
32
45
|
private
|
33
46
|
|
34
47
|
def get_circuit_for_uri(uri)
|
35
|
-
uri
|
36
|
-
|
37
|
-
if @circuits.key?(uri.origin)
|
48
|
+
if uri.respond_to?(:origin) && @circuits.key?(uri.origin)
|
38
49
|
@circuits[uri.origin]
|
39
50
|
else
|
40
51
|
@circuits[uri.to_s]
|
@@ -16,8 +16,12 @@ module HTTPX
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.extra_options(options)
|
19
|
-
options.merge(
|
20
|
-
|
19
|
+
options.merge(
|
20
|
+
circuit_breaker_max_attempts: 3,
|
21
|
+
circuit_breaker_reset_attempts_in: 60,
|
22
|
+
circuit_breaker_break_in: 60,
|
23
|
+
circuit_breaker_half_open_drip_rate: 1
|
24
|
+
)
|
21
25
|
end
|
22
26
|
|
23
27
|
module InstanceMethods
|
@@ -47,13 +51,13 @@ module HTTPX
|
|
47
51
|
|
48
52
|
# run all requests through the circuit breaker, see if the circuit is
|
49
53
|
# open for any of them.
|
50
|
-
real_requests = requests.
|
54
|
+
real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
|
51
55
|
short_circuit_response = @circuit_store.try_respond(req)
|
52
56
|
if short_circuit_response.nil?
|
53
57
|
real_reqs << req
|
54
58
|
next
|
55
59
|
end
|
56
|
-
short_circuit_responses[
|
60
|
+
short_circuit_responses[idx] = short_circuit_response
|
57
61
|
end
|
58
62
|
|
59
63
|
# run requests for the remainder
|
@@ -84,6 +88,9 @@ module HTTPX
|
|
84
88
|
end
|
85
89
|
elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
|
86
90
|
@circuit_store.try_open(request.uri, response)
|
91
|
+
else
|
92
|
+
@circuit_store.try_close(request.uri)
|
93
|
+
nil
|
87
94
|
end
|
88
95
|
end
|
89
96
|
end
|
@@ -5,7 +5,7 @@ module HTTPX
|
|
5
5
|
#
|
6
6
|
# This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
|
7
7
|
#
|
8
|
-
# https://gitlab.com/os85/httpx/wikis/
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Authorization#digest-auth
|
9
9
|
#
|
10
10
|
module DigestAuth
|
11
11
|
DigestError = Class.new(Error)
|
@@ -16,7 +16,7 @@ module HTTPX
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def load_dependencies(*)
|
19
|
-
require_relative "
|
19
|
+
require_relative "auth/digest"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
@@ -29,11 +29,11 @@ module HTTPX
|
|
29
29
|
end
|
30
30
|
|
31
31
|
module InstanceMethods
|
32
|
-
def
|
32
|
+
def digest_auth(user, password, hashed: false)
|
33
33
|
with(digest: Authentication::Digest.new(user, password, hashed: hashed))
|
34
34
|
end
|
35
35
|
|
36
|
-
|
36
|
+
private
|
37
37
|
|
38
38
|
def send_requests(*requests)
|
39
39
|
requests.flat_map do |request|
|
@@ -57,6 +57,6 @@ module HTTPX
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
register_plugin :
|
60
|
+
register_plugin :digest_auth, DigestAuth
|
61
61
|
end
|
62
62
|
end
|
@@ -48,7 +48,27 @@ module HTTPX
|
|
48
48
|
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
|
49
49
|
return response unless max_redirects.positive?
|
50
50
|
|
51
|
-
|
51
|
+
# build redirect request
|
52
|
+
redirect_uri = __get_location_from_response(response)
|
53
|
+
|
54
|
+
if response.status == 305 && options.respond_to?(:proxy)
|
55
|
+
# The requested resource MUST be accessed through the proxy given by
|
56
|
+
# the Location field. The Location field gives the URI of the proxy.
|
57
|
+
retry_options = options.merge(headers: redirect_request.headers,
|
58
|
+
proxy: { uri: redirect_uri },
|
59
|
+
body: redirect_request.body,
|
60
|
+
max_redirects: max_redirects - 1)
|
61
|
+
redirect_uri = redirect_request.uri
|
62
|
+
options = retry_options
|
63
|
+
else
|
64
|
+
|
65
|
+
# redirects are **ALWAYS** GET
|
66
|
+
retry_options = options.merge(headers: redirect_request.headers,
|
67
|
+
body: redirect_request.body,
|
68
|
+
max_redirects: max_redirects - 1)
|
69
|
+
end
|
70
|
+
|
71
|
+
retry_request = build_request("GET", redirect_uri, retry_options)
|
52
72
|
|
53
73
|
request.redirect_request = retry_request
|
54
74
|
|
@@ -83,29 +103,6 @@ module HTTPX
|
|
83
103
|
nil
|
84
104
|
end
|
85
105
|
|
86
|
-
def build_redirect_request(request, response, options)
|
87
|
-
redirect_uri = __get_location_from_response(response)
|
88
|
-
max_redirects = request.max_redirects
|
89
|
-
|
90
|
-
if response.status == 305 && options.respond_to?(:proxy)
|
91
|
-
# The requested resource MUST be accessed through the proxy given by
|
92
|
-
# the Location field. The Location field gives the URI of the proxy.
|
93
|
-
retry_options = options.merge(headers: request.headers,
|
94
|
-
proxy: { uri: redirect_uri },
|
95
|
-
body: request.body,
|
96
|
-
max_redirects: max_redirects - 1)
|
97
|
-
redirect_uri = request.url
|
98
|
-
else
|
99
|
-
|
100
|
-
# redirects are **ALWAYS** GET
|
101
|
-
retry_options = options.merge(headers: request.headers,
|
102
|
-
body: request.body,
|
103
|
-
max_redirects: max_redirects - 1)
|
104
|
-
end
|
105
|
-
|
106
|
-
build_request("GET", redirect_uri, retry_options)
|
107
|
-
end
|
108
|
-
|
109
106
|
def __get_location_from_response(response)
|
110
107
|
location_uri = URI(response.headers["location"])
|
111
108
|
location_uri = response.uri.merge(location_uri) if location_uri.relative?
|