httpx 0.21.0 → 1.2.1
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 +54 -45
- data/doc/release_notes/0_10_0.md +2 -2
- data/doc/release_notes/0_11_0.md +3 -5
- data/doc/release_notes/0_12_0.md +5 -5
- data/doc/release_notes/0_13_0.md +4 -4
- data/doc/release_notes/0_14_0.md +2 -2
- data/doc/release_notes/0_16_0.md +3 -3
- data/doc/release_notes/0_17_0.md +1 -1
- data/doc/release_notes/0_18_0.md +4 -4
- data/doc/release_notes/0_18_2.md +1 -1
- data/doc/release_notes/0_19_0.md +1 -1
- data/doc/release_notes/0_20_0.md +1 -1
- data/doc/release_notes/0_21_0.md +7 -5
- data/doc/release_notes/0_21_1.md +12 -0
- data/doc/release_notes/0_22_0.md +13 -0
- data/doc/release_notes/0_22_1.md +11 -0
- data/doc/release_notes/0_22_2.md +5 -0
- data/doc/release_notes/0_22_3.md +55 -0
- data/doc/release_notes/0_22_4.md +6 -0
- data/doc/release_notes/0_22_5.md +6 -0
- data/doc/release_notes/0_23_0.md +42 -0
- data/doc/release_notes/0_23_1.md +5 -0
- data/doc/release_notes/0_23_2.md +5 -0
- data/doc/release_notes/0_23_3.md +6 -0
- data/doc/release_notes/0_23_4.md +5 -0
- data/doc/release_notes/0_24_0.md +48 -0
- data/doc/release_notes/0_24_1.md +12 -0
- data/doc/release_notes/0_24_2.md +12 -0
- data/doc/release_notes/0_24_3.md +12 -0
- data/doc/release_notes/0_24_4.md +18 -0
- data/doc/release_notes/0_24_5.md +6 -0
- data/doc/release_notes/0_24_6.md +5 -0
- data/doc/release_notes/0_24_7.md +10 -0
- data/doc/release_notes/1_0_0.md +60 -0
- data/doc/release_notes/1_0_1.md +5 -0
- data/doc/release_notes/1_0_2.md +7 -0
- data/doc/release_notes/1_1_0.md +32 -0
- data/doc/release_notes/1_1_1.md +17 -0
- data/doc/release_notes/1_1_2.md +12 -0
- data/doc/release_notes/1_1_3.md +18 -0
- data/doc/release_notes/1_1_4.md +6 -0
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/doc/release_notes/1_2_1.md +6 -0
- data/lib/httpx/adapters/datadog.rb +100 -106
- data/lib/httpx/adapters/faraday.rb +143 -107
- data/lib/httpx/adapters/sentry.rb +26 -7
- data/lib/httpx/adapters/webmock.rb +33 -17
- data/lib/httpx/altsvc.rb +61 -24
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/buffer.rb +12 -0
- data/lib/httpx/callbacks.rb +5 -3
- data/lib/httpx/chainable.rb +54 -39
- data/lib/httpx/connection/http1.rb +62 -37
- data/lib/httpx/connection/http2.rb +16 -27
- data/lib/httpx/connection.rb +213 -120
- data/lib/httpx/domain_name.rb +10 -13
- data/lib/httpx/errors.rb +34 -2
- data/lib/httpx/extensions.rb +4 -134
- data/lib/httpx/io/ssl.rb +77 -71
- data/lib/httpx/io/tcp.rb +46 -70
- data/lib/httpx/io/udp.rb +18 -52
- data/lib/httpx/io/unix.rb +6 -13
- data/lib/httpx/io.rb +3 -9
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +168 -110
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +13 -14
- 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_sdk_authentication.rb +1 -3
- data/lib/httpx/plugins/aws_sigv4.rb +5 -6
- data/lib/httpx/plugins/basic_auth.rb +29 -0
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/callbacks.rb +91 -0
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +40 -16
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +14 -5
- data/lib/httpx/plugins/circuit_breaker.rb +30 -7
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +20 -10
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +11 -12
- data/lib/httpx/plugins/expect.rb +15 -13
- data/lib/httpx/plugins/follow_redirects.rb +71 -29
- data/lib/httpx/plugins/grpc/call.rb +2 -3
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
- data/lib/httpx/plugins/grpc/message.rb +7 -37
- data/lib/httpx/plugins/grpc.rb +35 -29
- data/lib/httpx/plugins/h2c.rb +25 -18
- data/lib/httpx/plugins/internal_telemetry.rb +16 -0
- data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
- data/lib/httpx/plugins/oauth.rb +170 -0
- data/lib/httpx/plugins/persistent.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +15 -10
- data/lib/httpx/plugins/proxy/socks4.rb +8 -6
- data/lib/httpx/plugins/proxy/socks5.rb +10 -8
- data/lib/httpx/plugins/proxy.rb +69 -67
- data/lib/httpx/plugins/push_promise.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +3 -1
- data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
- data/lib/httpx/plugins/response_cache/store.rb +34 -17
- data/lib/httpx/plugins/response_cache.rb +6 -6
- data/lib/httpx/plugins/retries.rb +61 -12
- data/lib/httpx/plugins/ssrf_filter.rb +142 -0
- data/lib/httpx/plugins/stream.rb +27 -32
- data/lib/httpx/plugins/upgrade/h2.rb +4 -4
- data/lib/httpx/plugins/upgrade.rb +8 -10
- data/lib/httpx/plugins/webdav.rb +10 -8
- data/lib/httpx/pool.rb +85 -23
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +158 -0
- data/lib/httpx/request.rb +86 -121
- data/lib/httpx/resolver/https.rb +54 -17
- data/lib/httpx/resolver/multi.rb +8 -12
- data/lib/httpx/resolver/native.rb +163 -70
- data/lib/httpx/resolver/resolver.rb +28 -13
- data/lib/httpx/resolver/system.rb +15 -10
- data/lib/httpx/resolver.rb +38 -16
- data/lib/httpx/response/body.rb +242 -0
- data/lib/httpx/response/buffer.rb +96 -0
- data/lib/httpx/response.rb +113 -211
- data/lib/httpx/selector.rb +2 -4
- data/lib/httpx/session.rb +91 -64
- data/lib/httpx/session_extensions.rb +4 -1
- data/lib/httpx/timers.rb +28 -8
- data/lib/httpx/transcoder/body.rb +0 -2
- data/lib/httpx/transcoder/chunker.rb +0 -1
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +52 -33
- data/lib/httpx/transcoder/gzip.rb +74 -0
- data/lib/httpx/transcoder/json.rb +2 -5
- 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/utils/inflater.rb +19 -0
- data/lib/httpx/transcoder/xml.rb +0 -5
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/utils.rb +36 -16
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +12 -14
- data/sig/altsvc.rbs +33 -0
- data/sig/buffer.rbs +1 -0
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +10 -9
- data/sig/connection/http1.rbs +5 -4
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +46 -24
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +5 -4
- data/sig/io/ssl.rbs +26 -0
- data/sig/io/tcp.rbs +60 -0
- data/sig/io/udp.rbs +20 -0
- data/sig/io/unix.rbs +10 -0
- data/sig/options.rbs +28 -12
- data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
- data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
- 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/callbacks.rbs +38 -0
- data/sig/plugins/circuit_breaker.rbs +13 -3
- data/sig/plugins/compression.rbs +6 -4
- data/sig/plugins/cookies/jar.rbs +2 -2
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
- data/sig/plugins/follow_redirects.rbs +11 -2
- data/sig/plugins/grpc/call.rbs +19 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +2 -32
- data/sig/plugins/h2c.rbs +1 -1
- data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
- data/sig/plugins/oauth.rbs +54 -0
- data/sig/plugins/proxy/socks4.rbs +4 -4
- data/sig/plugins/proxy/socks5.rbs +2 -2
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/proxy.rbs +10 -4
- data/sig/plugins/response_cache.rbs +12 -3
- data/sig/plugins/retries.rbs +28 -8
- data/sig/plugins/stream.rbs +24 -17
- data/sig/plugins/upgrade.rbs +5 -3
- data/sig/pool.rbs +5 -4
- data/sig/request/body.rbs +40 -0
- data/sig/request.rbs +12 -28
- data/sig/resolver/https.rbs +7 -2
- data/sig/resolver/native.rbs +10 -4
- data/sig/resolver/resolver.rbs +6 -4
- data/sig/resolver/system.rbs +2 -0
- data/sig/resolver.rbs +9 -5
- data/sig/response/body.rbs +53 -0
- data/sig/response/buffer.rbs +24 -0
- data/sig/response.rbs +17 -38
- data/sig/session.rbs +24 -18
- data/sig/timers.rbs +17 -7
- 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 -12
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +29 -0
- data/sig/transcoder/utils/inflater.rbs +12 -0
- data/sig/transcoder/xml.rbs +1 -1
- data/sig/transcoder.rbs +22 -7
- data/sig/utils.rbs +2 -0
- metadata +127 -40
- data/lib/httpx/plugins/authentication.rb +0 -20
- data/lib/httpx/plugins/basic_authentication.rb +0 -30
- data/lib/httpx/plugins/compression/brotli.rb +0 -54
- data/lib/httpx/plugins/compression/deflate.rb +0 -49
- data/lib/httpx/plugins/compression/gzip.rb +0 -88
- data/lib/httpx/plugins/compression.rb +0 -164
- data/lib/httpx/plugins/multipart/decoder.rb +0 -187
- data/lib/httpx/plugins/multipart.rb +0 -84
- data/lib/httpx/registry.rb +0 -85
- data/sig/plugins/authentication.rbs +0 -11
- 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/registry.rbs +0 -13
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -8,12 +8,11 @@ module HTTPX
|
|
8
8
|
module Plugins
|
9
9
|
module Authentication
|
10
10
|
class Digest
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(user, password, **)
|
11
|
+
def initialize(user, password, hashed: false, **)
|
14
12
|
@user = user
|
15
13
|
@password = password
|
16
14
|
@nonce = 0
|
15
|
+
@hashed = hashed
|
17
16
|
end
|
18
17
|
|
19
18
|
def can_authenticate?(authenticate)
|
@@ -21,7 +20,7 @@ module HTTPX
|
|
21
20
|
end
|
22
21
|
|
23
22
|
def authenticate(request, authenticate)
|
24
|
-
"Digest #{generate_header(request.verb
|
23
|
+
"Digest #{generate_header(request.verb, request.path, authenticate)}"
|
25
24
|
end
|
26
25
|
|
27
26
|
private
|
@@ -30,9 +29,8 @@ module HTTPX
|
|
30
29
|
# discard first token, it's Digest
|
31
30
|
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
32
31
|
|
33
|
-
params =
|
34
|
-
|
35
|
-
.map { |k, v| [k, v.delete("\"")] }]
|
32
|
+
params = auth_info.split(/ *, */)
|
33
|
+
.to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
|
36
34
|
nonce = params["nonce"]
|
37
35
|
nc = next_nonce
|
38
36
|
|
@@ -45,7 +43,6 @@ module HTTPX
|
|
45
43
|
raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
|
46
44
|
|
47
45
|
sess = Regexp.last_match(2)
|
48
|
-
params.delete("algorithm")
|
49
46
|
else
|
50
47
|
algorithm = ::Digest::MD5
|
51
48
|
end
|
@@ -56,11 +53,13 @@ module HTTPX
|
|
56
53
|
end
|
57
54
|
|
58
55
|
a1 = if sess
|
59
|
-
[
|
60
|
-
|
61
|
-
|
56
|
+
[
|
57
|
+
(@hashed ? @password : algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}")),
|
58
|
+
nonce,
|
59
|
+
cnonce,
|
60
|
+
].join ":"
|
62
61
|
else
|
63
|
-
"#{@user}:#{params["realm"]}:#{@password}"
|
62
|
+
@hashed ? @password : "#{@user}:#{params["realm"]}:#{@password}"
|
64
63
|
end
|
65
64
|
|
66
65
|
ha1 = algorithm.hexdigest(a1)
|
@@ -77,11 +76,11 @@ module HTTPX
|
|
77
76
|
%(response="#{algorithm.hexdigest(request_digest)}"),
|
78
77
|
]
|
79
78
|
header << %(realm="#{params["realm"]}") if params.key?("realm")
|
80
|
-
header << %(algorithm=#{params["algorithm"]}
|
81
|
-
header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
|
79
|
+
header << %(algorithm=#{params["algorithm"]}) if params.key?("algorithm")
|
82
80
|
header << %(cnonce="#{cnonce}") if cnonce
|
83
81
|
header << %(nc=#{nc})
|
84
82
|
header << %(qop=#{qop}) if qop
|
83
|
+
header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
|
85
84
|
header.join ", "
|
86
85
|
end
|
87
86
|
|
@@ -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#auth
|
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,9 +5,9 @@ module HTTPX
|
|
5
5
|
#
|
6
6
|
# This plugin adds AWS Sigv4 authentication.
|
7
7
|
#
|
8
|
-
# https://docs.aws.amazon.com/
|
8
|
+
# https://docs.aws.amazon.com/IAM/latest/UserGuide/signing-elements.html
|
9
9
|
#
|
10
|
-
# https://gitlab.com/
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/AWS-SigV4
|
11
11
|
#
|
12
12
|
module AWSSigV4
|
13
13
|
Credentials = Struct.new(:username, :password, :security_token)
|
@@ -71,7 +71,7 @@ module HTTPX
|
|
71
71
|
end.join
|
72
72
|
|
73
73
|
# canonical request
|
74
|
-
creq = "#{request.verb
|
74
|
+
creq = "#{request.verb}" \
|
75
75
|
"\n#{request.canonical_path}" \
|
76
76
|
"\n#{request.canonical_query}" \
|
77
77
|
"\n#{canonical_headers}" \
|
@@ -115,7 +115,7 @@ module HTTPX
|
|
115
115
|
elsif value.respond_to?(:each)
|
116
116
|
digest = OpenSSL::Digest.new(@algorithm)
|
117
117
|
|
118
|
-
mb_buffer = value.each.
|
118
|
+
mb_buffer = value.each.with_object("".b) do |chunk, buffer|
|
119
119
|
buffer << chunk
|
120
120
|
break if buffer.bytesize >= 1024 * 1024
|
121
121
|
end
|
@@ -146,7 +146,6 @@ module HTTPX
|
|
146
146
|
|
147
147
|
def configure(klass)
|
148
148
|
klass.plugin(:expect)
|
149
|
-
klass.plugin(:compression)
|
150
149
|
end
|
151
150
|
end
|
152
151
|
|
@@ -186,7 +185,7 @@ module HTTPX
|
|
186
185
|
def canonical_query
|
187
186
|
params = query.split("&")
|
188
187
|
# params = params.map { |p| p.match(/=/) ? p : p + '=' }
|
189
|
-
# From: https://docs.aws.amazon.com/
|
188
|
+
# From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
|
190
189
|
# Sort the parameter names by character code point in ascending order.
|
191
190
|
# Parameters with duplicate names should be sorted by value.
|
192
191
|
#
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds helper methods to implement HTTP Basic Auth (https://datatracker.ietf.org/doc/html/rfc7617)
|
7
|
+
#
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Auth#basic-auth
|
9
|
+
#
|
10
|
+
module BasicAuth
|
11
|
+
class << self
|
12
|
+
def load_dependencies(_klass)
|
13
|
+
require_relative "auth/basic"
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure(klass)
|
17
|
+
klass.plugin(:auth)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def basic_auth(user, password)
|
23
|
+
authorization(Authentication::Basic.new(user, password).authenticate)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
register_plugin :basic_auth, BasicAuth
|
28
|
+
end
|
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
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds suppoort for callbacks around the request/response lifecycle.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/os85/httpx/-/wikis/Events
|
9
|
+
#
|
10
|
+
module Callbacks
|
11
|
+
# connection closed user-space errors happen after errors can be surfaced to requests,
|
12
|
+
# so they need to pierce through the scheduler, which is only possible by simulating an
|
13
|
+
# interrupt.
|
14
|
+
class CallbackError < Exception; end # rubocop:disable Lint/InheritException
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
include HTTPX::Callbacks
|
18
|
+
|
19
|
+
%i[
|
20
|
+
connection_opened connection_closed
|
21
|
+
request_error
|
22
|
+
request_started request_body_chunk request_completed
|
23
|
+
response_started response_body_chunk response_completed
|
24
|
+
].each do |meth|
|
25
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
26
|
+
def on_#{meth}(&blk) # def on_connection_opened(&blk)
|
27
|
+
on(:#{meth}, &blk) # on(:connection_opened, &blk)
|
28
|
+
end # end
|
29
|
+
MOD
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def init_connection(uri, options)
|
35
|
+
connection = super
|
36
|
+
connection.on(:open) do
|
37
|
+
emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
|
38
|
+
end
|
39
|
+
connection.on(:close) do
|
40
|
+
emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
|
41
|
+
end
|
42
|
+
|
43
|
+
connection
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_request_callbacks(request)
|
47
|
+
super
|
48
|
+
|
49
|
+
request.on(:headers) do
|
50
|
+
emit_or_callback_error(:request_started, request)
|
51
|
+
end
|
52
|
+
request.on(:body_chunk) do |chunk|
|
53
|
+
emit_or_callback_error(:request_body_chunk, request, chunk)
|
54
|
+
end
|
55
|
+
request.on(:done) do
|
56
|
+
emit_or_callback_error(:request_completed, request)
|
57
|
+
end
|
58
|
+
|
59
|
+
request.on(:response_started) do |res|
|
60
|
+
if res.is_a?(Response)
|
61
|
+
emit_or_callback_error(:response_started, request, res)
|
62
|
+
res.on(:chunk_received) do |chunk|
|
63
|
+
emit_or_callback_error(:response_body_chunk, request, res, chunk)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
emit_or_callback_error(:request_error, request, res.error)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
request.on(:response) do |res|
|
70
|
+
emit_or_callback_error(:response_completed, request, res)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def emit_or_callback_error(*args)
|
75
|
+
emit(*args)
|
76
|
+
rescue StandardError => e
|
77
|
+
ex = CallbackError.new(e.message)
|
78
|
+
ex.set_backtrace(e.backtrace)
|
79
|
+
raise ex
|
80
|
+
end
|
81
|
+
|
82
|
+
def receive_requests(*)
|
83
|
+
super
|
84
|
+
rescue CallbackError => e
|
85
|
+
raise e.cause
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
register_plugin :callbacks, Callbacks
|
90
|
+
end
|
91
|
+
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
|
@@ -13,18 +13,29 @@ module HTTPX::Plugins::CircuitBreaker
|
|
13
13
|
options.circuit_breaker_half_open_drip_rate
|
14
14
|
)
|
15
15
|
end
|
16
|
+
@circuits_mutex = Thread::Mutex.new
|
16
17
|
end
|
17
18
|
|
18
19
|
def try_open(uri, response)
|
19
|
-
circuit = get_circuit_for_uri(uri)
|
20
|
+
circuit = @circuits_mutex.synchronize { get_circuit_for_uri(uri) }
|
20
21
|
|
21
22
|
circuit.try_open(response)
|
22
23
|
end
|
23
24
|
|
25
|
+
def try_close(uri)
|
26
|
+
circuit = @circuits_mutex.synchronize do
|
27
|
+
return unless @circuits.key?(uri.origin) || @circuits.key?(uri.to_s)
|
28
|
+
|
29
|
+
get_circuit_for_uri(uri)
|
30
|
+
end
|
31
|
+
|
32
|
+
circuit.try_close
|
33
|
+
end
|
34
|
+
|
24
35
|
# if circuit is open, it'll respond with the stored response.
|
25
36
|
# if not, nil.
|
26
37
|
def try_respond(request)
|
27
|
-
circuit = get_circuit_for_uri(request.uri)
|
38
|
+
circuit = @circuits_mutex.synchronize { get_circuit_for_uri(request.uri) }
|
28
39
|
|
29
40
|
circuit.respond
|
30
41
|
end
|
@@ -32,9 +43,7 @@ module HTTPX::Plugins::CircuitBreaker
|
|
32
43
|
private
|
33
44
|
|
34
45
|
def get_circuit_for_uri(uri)
|
35
|
-
uri
|
36
|
-
|
37
|
-
if @circuits.key?(uri.origin)
|
46
|
+
if uri.respond_to?(:origin) && @circuits.key?(uri.origin)
|
38
47
|
@circuits[uri.origin]
|
39
48
|
else
|
40
49
|
@circuits[uri.to_s]
|
@@ -5,7 +5,7 @@ module HTTPX
|
|
5
5
|
#
|
6
6
|
# This plugin implements a circuit breaker around connection errors.
|
7
7
|
#
|
8
|
-
# https://gitlab.com/
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Circuit-Breaker
|
9
9
|
#
|
10
10
|
module CircuitBreaker
|
11
11
|
using URIExtensions
|
@@ -16,11 +16,17 @@ 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
|
28
|
+
include HTTPX::Callbacks
|
29
|
+
|
24
30
|
def initialize(*)
|
25
31
|
super
|
26
32
|
@circuit_store = CircuitStore.new(@options)
|
@@ -31,19 +37,29 @@ module HTTPX
|
|
31
37
|
@circuit_store = orig.instance_variable_get(:@circuit_store).dup
|
32
38
|
end
|
33
39
|
|
40
|
+
%i[circuit_open].each do |meth|
|
41
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
42
|
+
def on_#{meth}(&blk) # def on_circuit_open(&blk)
|
43
|
+
on(:#{meth}, &blk) # on(:circuit_open, &blk)
|
44
|
+
end # end
|
45
|
+
MOD
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
34
50
|
def send_requests(*requests)
|
35
51
|
# @type var short_circuit_responses: Array[response]
|
36
52
|
short_circuit_responses = []
|
37
53
|
|
38
54
|
# run all requests through the circuit breaker, see if the circuit is
|
39
55
|
# open for any of them.
|
40
|
-
real_requests = requests.
|
56
|
+
real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
|
41
57
|
short_circuit_response = @circuit_store.try_respond(req)
|
42
58
|
if short_circuit_response.nil?
|
43
59
|
real_reqs << req
|
44
60
|
next
|
45
61
|
end
|
46
|
-
short_circuit_responses[
|
62
|
+
short_circuit_responses[idx] = short_circuit_response
|
47
63
|
end
|
48
64
|
|
49
65
|
# run requests for the remainder
|
@@ -59,6 +75,12 @@ module HTTPX
|
|
59
75
|
end
|
60
76
|
|
61
77
|
def on_response(request, response)
|
78
|
+
emit(:circuit_open, request) if try_circuit_open(request, response)
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def try_circuit_open(request, response)
|
62
84
|
if response.is_a?(ErrorResponse)
|
63
85
|
case response.error
|
64
86
|
when RequestTimeoutError
|
@@ -68,9 +90,10 @@ module HTTPX
|
|
68
90
|
end
|
69
91
|
elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
|
70
92
|
@circuit_store.try_open(request.uri, response)
|
93
|
+
else
|
94
|
+
@circuit_store.try_close(request.uri)
|
95
|
+
nil
|
71
96
|
end
|
72
|
-
|
73
|
-
super
|
74
97
|
end
|
75
98
|
end
|
76
99
|
|
@@ -9,7 +9,7 @@ module HTTPX
|
|
9
9
|
#
|
10
10
|
# It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
|
11
11
|
#
|
12
|
-
# https://gitlab.com/
|
12
|
+
# https://gitlab.com/os85/httpx/wikis/Cookies
|
13
13
|
#
|
14
14
|
module Cookies
|
15
15
|
def self.load_dependencies(*)
|
@@ -71,22 +71,32 @@ module HTTPX
|
|
71
71
|
end
|
72
72
|
|
73
73
|
module OptionsMethods
|
74
|
-
def
|
75
|
-
super
|
74
|
+
def option_headers(*)
|
75
|
+
value = super
|
76
|
+
|
77
|
+
merge_cookie_in_jar(value.delete("cookie"), @cookies) if defined?(@cookies) && value.key?("cookie")
|
76
78
|
|
77
|
-
|
79
|
+
value
|
80
|
+
end
|
78
81
|
|
79
|
-
|
82
|
+
def option_cookies(value)
|
83
|
+
jar = value.is_a?(Jar) ? value : Jar.new(value)
|
84
|
+
|
85
|
+
merge_cookie_in_jar(@headers.delete("cookie"), jar) if defined?(@headers) && @headers.key?("cookie")
|
86
|
+
|
87
|
+
jar
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def merge_cookie_in_jar(cookies, jar)
|
93
|
+
cookies.each do |ck|
|
80
94
|
ck.split(/ *; */).each do |cookie|
|
81
95
|
name, value = cookie.split("=", 2)
|
82
|
-
|
96
|
+
jar.add(Cookie.new(name, value))
|
83
97
|
end
|
84
98
|
end
|
85
99
|
end
|
86
|
-
|
87
|
-
def option_cookies(value)
|
88
|
-
value.is_a?(Jar) ? value : Jar.new(value)
|
89
|
-
end
|
90
100
|
end
|
91
101
|
end
|
92
102
|
register_plugin :cookies, Cookies
|