httpx 0.20.0 → 1.3.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 +5 -5
- 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_19_8.md +1 -1
- data/doc/release_notes/0_20_0.md +2 -2
- data/doc/release_notes/0_20_1.md +5 -0
- data/doc/release_notes/0_20_2.md +7 -0
- data/doc/release_notes/0_20_3.md +6 -0
- 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 +96 -0
- 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/doc/release_notes/1_2_2.md +10 -0
- data/doc/release_notes/1_2_3.md +16 -0
- data/doc/release_notes/1_2_4.md +8 -0
- data/doc/release_notes/1_2_5.md +7 -0
- data/doc/release_notes/1_2_6.md +13 -0
- data/doc/release_notes/1_3_0.md +18 -0
- data/doc/release_notes/1_3_1.md +17 -0
- data/lib/httpx/adapters/datadog.rb +215 -122
- data/lib/httpx/adapters/faraday.rb +145 -107
- data/lib/httpx/adapters/sentry.rb +26 -7
- data/lib/httpx/adapters/webmock.rb +34 -18
- data/lib/httpx/altsvc.rb +63 -26
- 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 +75 -44
- data/lib/httpx/connection/http2.rb +31 -38
- data/lib/httpx/connection.rb +287 -117
- data/lib/httpx/domain_name.rb +10 -13
- data/lib/httpx/errors.rb +52 -2
- data/lib/httpx/extensions.rb +24 -131
- data/lib/httpx/io/ssl.rb +83 -77
- data/lib/httpx/io/tcp.rb +48 -71
- data/lib/httpx/io/udp.rb +18 -52
- data/lib/httpx/io/unix.rb +10 -15
- data/lib/httpx/io.rb +3 -9
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +176 -118
- data/lib/httpx/parser/http1.rb +4 -0
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -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 +4 -3
- data/lib/httpx/plugins/aws_sigv4.rb +12 -9
- 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 +100 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
- data/lib/httpx/plugins/circuit_breaker.rb +148 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +30 -17
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
- data/lib/httpx/plugins/expect.rb +21 -14
- data/lib/httpx/plugins/follow_redirects.rb +140 -41
- 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 +36 -29
- data/lib/httpx/plugins/h2c.rb +26 -19
- 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 +175 -0
- data/lib/httpx/plugins/persistent.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +23 -13
- data/lib/httpx/plugins/proxy/socks4.rb +9 -7
- data/lib/httpx/plugins/proxy/socks5.rb +11 -9
- data/lib/httpx/plugins/proxy.rb +80 -61
- data/lib/httpx/plugins/push_promise.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +5 -1
- data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
- data/lib/httpx/plugins/response_cache/store.rb +62 -25
- data/lib/httpx/plugins/response_cache.rb +105 -12
- data/lib/httpx/plugins/retries.rb +87 -17
- data/lib/httpx/plugins/ssrf_filter.rb +145 -0
- data/lib/httpx/plugins/stream.rb +27 -23
- data/lib/httpx/plugins/upgrade/h2.rb +4 -4
- data/lib/httpx/plugins/upgrade.rb +8 -10
- data/lib/httpx/plugins/webdav.rb +80 -0
- data/lib/httpx/pool/synch_pool.rb +93 -0
- data/lib/httpx/pool.rb +102 -27
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +154 -0
- data/lib/httpx/request.rb +130 -146
- data/lib/httpx/resolver/https.rb +62 -27
- data/lib/httpx/resolver/multi.rb +9 -13
- data/lib/httpx/resolver/native.rb +192 -76
- data/lib/httpx/resolver/resolver.rb +34 -9
- data/lib/httpx/resolver/system.rb +16 -11
- 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 +159 -217
- data/lib/httpx/selector.rb +9 -4
- data/lib/httpx/session.rb +137 -89
- data/lib/httpx/session_extensions.rb +4 -1
- data/lib/httpx/timers.rb +34 -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 +21 -8
- data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
- data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
- 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 +52 -0
- data/lib/httpx/transcoder.rb +5 -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 +2 -1
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +11 -9
- data/sig/connection/http1.rbs +8 -7
- data/sig/connection/http2.rbs +19 -19
- data/sig/connection.rbs +64 -24
- data/sig/errors.rbs +22 -3
- data/sig/httpx.rbs +5 -4
- data/sig/io/ssl.rbs +27 -0
- data/sig/io/tcp.rbs +60 -0
- data/sig/io/udp.rbs +20 -0
- data/sig/io/unix.rbs +27 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +32 -22
- data/sig/parser/http1.rbs +1 -1
- 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 +71 -0
- data/sig/plugins/compression.rbs +7 -5
- 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 +18 -4
- 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 +7 -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/http.rbs +3 -0
- data/sig/plugins/proxy/socks4.rbs +9 -6
- data/sig/plugins/proxy/socks5.rbs +10 -6
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/proxy.rbs +13 -5
- data/sig/plugins/push_promise.rbs +3 -3
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache.rbs +36 -7
- data/sig/plugins/retries.rbs +30 -8
- data/sig/plugins/stream.rbs +24 -17
- data/sig/plugins/upgrade.rbs +5 -3
- data/sig/pool.rbs +10 -7
- data/sig/request/body.rbs +38 -0
- data/sig/request.rbs +15 -24
- data/sig/resolver/https.rbs +8 -3
- data/sig/resolver/native.rbs +17 -4
- data/sig/resolver/resolver.rbs +8 -6
- 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 +24 -39
- data/sig/selector.rbs +1 -1
- data/sig/session.rbs +29 -18
- data/sig/timers.rbs +18 -8
- 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 +8 -3
- data/sig/{plugins → transcoder}/multipart.rbs +15 -19
- 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 +22 -0
- data/sig/transcoder.rbs +24 -9
- data/sig/utils.rbs +8 -2
- metadata +163 -41
- 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 -12
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -5,13 +5,14 @@ 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)
|
14
14
|
|
15
|
+
# Signs requests using the AWS sigv4 signing.
|
15
16
|
class Signer
|
16
17
|
def initialize(
|
17
18
|
service:,
|
@@ -71,7 +72,7 @@ module HTTPX
|
|
71
72
|
end.join
|
72
73
|
|
73
74
|
# canonical request
|
74
|
-
creq = "#{request.verb
|
75
|
+
creq = "#{request.verb}" \
|
75
76
|
"\n#{request.canonical_path}" \
|
76
77
|
"\n#{request.canonical_query}" \
|
77
78
|
"\n#{canonical_headers}" \
|
@@ -115,7 +116,7 @@ module HTTPX
|
|
115
116
|
elsif value.respond_to?(:each)
|
116
117
|
digest = OpenSSL::Digest.new(@algorithm)
|
117
118
|
|
118
|
-
mb_buffer = value.each.
|
119
|
+
mb_buffer = value.each.with_object("".b) do |chunk, buffer|
|
119
120
|
buffer << chunk
|
120
121
|
break if buffer.bytesize >= 1024 * 1024
|
121
122
|
end
|
@@ -146,10 +147,12 @@ module HTTPX
|
|
146
147
|
|
147
148
|
def configure(klass)
|
148
149
|
klass.plugin(:expect)
|
149
|
-
klass.plugin(:compression)
|
150
150
|
end
|
151
151
|
end
|
152
152
|
|
153
|
+
# adds support for the following options:
|
154
|
+
#
|
155
|
+
# :sigv4_signer :: instance of HTTPX::Plugins::AWSSigV4 used to sign requests.
|
153
156
|
module OptionsMethods
|
154
157
|
def option_sigv4_signer(value)
|
155
158
|
value.is_a?(Signer) ? value : Signer.new(value)
|
@@ -161,7 +164,7 @@ module HTTPX
|
|
161
164
|
with(sigv4_signer: Signer.new(**options))
|
162
165
|
end
|
163
166
|
|
164
|
-
def build_request(
|
167
|
+
def build_request(*)
|
165
168
|
request = super
|
166
169
|
|
167
170
|
return request if request.headers.key?("authorization")
|
@@ -186,7 +189,7 @@ module HTTPX
|
|
186
189
|
def canonical_query
|
187
190
|
params = query.split("&")
|
188
191
|
# params = params.map { |p| p.match(/=/) ? p : p + '=' }
|
189
|
-
# From: https://docs.aws.amazon.com/
|
192
|
+
# From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
|
190
193
|
# Sort the parameter names by character code point in ascending order.
|
191
194
|
# Parameters with duplicate names should be sorted by value.
|
192
195
|
#
|
@@ -198,8 +201,8 @@ module HTTPX
|
|
198
201
|
params.each.with_index.sort do |a, b|
|
199
202
|
a, a_offset = a
|
200
203
|
b, b_offset = b
|
201
|
-
a_name, a_value = a.split("=")
|
202
|
-
b_name, b_value = b.split("=")
|
204
|
+
a_name, a_value = a.split("=", 2)
|
205
|
+
b_name, b_value = b.split("=", 2)
|
203
206
|
if a_name == b_name
|
204
207
|
if a_value == b_value
|
205
208
|
a_offset <=> b_offset
|
@@ -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
|
@@ -0,0 +1,100 @@
|
|
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 = circuit_breaker_half_open_drip_rate
|
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
|
23
|
+
@state = :closed
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond
|
27
|
+
try_close
|
28
|
+
|
29
|
+
case @state
|
30
|
+
when :closed
|
31
|
+
nil
|
32
|
+
when :half_open
|
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
|
40
|
+
|
41
|
+
@response
|
42
|
+
when :open
|
43
|
+
|
44
|
+
@response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def try_open(response)
|
49
|
+
case @state
|
50
|
+
when :closed
|
51
|
+
now = Utils.now
|
52
|
+
|
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
|
59
|
+
|
60
|
+
@attempts += 1
|
61
|
+
|
62
|
+
return unless @attempts >= @max_attempts
|
63
|
+
|
64
|
+
@state = :open
|
65
|
+
@opened_at = now
|
66
|
+
@response = response
|
67
|
+
when :half_open
|
68
|
+
# open immediately
|
69
|
+
|
70
|
+
@state = :open
|
71
|
+
@attempted_at = @opened_at = Utils.now
|
72
|
+
@response = response
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def try_close
|
77
|
+
case @state
|
78
|
+
when :closed
|
79
|
+
nil
|
80
|
+
when :half_open
|
81
|
+
|
82
|
+
# do not close circuit unless attempts exhausted
|
83
|
+
return unless @attempts >= @max_attempts
|
84
|
+
|
85
|
+
# reset!
|
86
|
+
@attempts = 0
|
87
|
+
@opened_at = @attempted_at = @response = nil
|
88
|
+
@state = :closed
|
89
|
+
|
90
|
+
when :open
|
91
|
+
if Utils.elapsed_time(@opened_at) > @break_in
|
92
|
+
@state = :half_open
|
93
|
+
@attempts = 0
|
94
|
+
@real_attempts = 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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
|
+
@circuits_mutex = Thread::Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def try_open(uri, response)
|
20
|
+
circuit = @circuits_mutex.synchronize { get_circuit_for_uri(uri) }
|
21
|
+
|
22
|
+
circuit.try_open(response)
|
23
|
+
end
|
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
|
+
|
35
|
+
# if circuit is open, it'll respond with the stored response.
|
36
|
+
# if not, nil.
|
37
|
+
def try_respond(request)
|
38
|
+
circuit = @circuits_mutex.synchronize { get_circuit_for_uri(request.uri) }
|
39
|
+
|
40
|
+
circuit.respond
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_circuit_for_uri(uri)
|
46
|
+
if uri.respond_to?(:origin) && @circuits.key?(uri.origin)
|
47
|
+
@circuits[uri.origin]
|
48
|
+
else
|
49
|
+
@circuits[uri.to_s]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,148 @@
|
|
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/os85/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(
|
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
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
include HTTPX::Callbacks
|
29
|
+
|
30
|
+
def initialize(*)
|
31
|
+
super
|
32
|
+
@circuit_store = CircuitStore.new(@options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize_dup(orig)
|
36
|
+
super
|
37
|
+
@circuit_store = orig.instance_variable_get(:@circuit_store).dup
|
38
|
+
end
|
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
|
+
|
50
|
+
def send_requests(*requests)
|
51
|
+
# @type var short_circuit_responses: Array[response]
|
52
|
+
short_circuit_responses = []
|
53
|
+
|
54
|
+
# run all requests through the circuit breaker, see if the circuit is
|
55
|
+
# open for any of them.
|
56
|
+
real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
|
57
|
+
short_circuit_response = @circuit_store.try_respond(req)
|
58
|
+
if short_circuit_response.nil?
|
59
|
+
real_reqs << req
|
60
|
+
next
|
61
|
+
end
|
62
|
+
short_circuit_responses[idx] = short_circuit_response
|
63
|
+
end
|
64
|
+
|
65
|
+
# run requests for the remainder
|
66
|
+
unless real_requests.empty?
|
67
|
+
responses = super(*real_requests)
|
68
|
+
|
69
|
+
real_requests.each_with_index do |request, idx|
|
70
|
+
short_circuit_responses[requests.index(request)] = responses[idx]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
short_circuit_responses
|
75
|
+
end
|
76
|
+
|
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)
|
84
|
+
if response.is_a?(ErrorResponse)
|
85
|
+
case response.error
|
86
|
+
when RequestTimeoutError
|
87
|
+
@circuit_store.try_open(request.uri, response)
|
88
|
+
else
|
89
|
+
@circuit_store.try_open(request.origin, response)
|
90
|
+
end
|
91
|
+
elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
|
92
|
+
@circuit_store.try_open(request.uri, response)
|
93
|
+
else
|
94
|
+
@circuit_store.try_close(request.uri)
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# adds support for the following options:
|
101
|
+
#
|
102
|
+
# :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
|
103
|
+
# :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
|
104
|
+
# :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
|
105
|
+
# (i.e. <tt>->(res) { res.status == 429 } </tt>)
|
106
|
+
# :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
|
107
|
+
# (defaults to <tt><60</tt>).
|
108
|
+
# :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
|
109
|
+
# (defaults to <tt>1</tt>).
|
110
|
+
module OptionsMethods
|
111
|
+
def option_circuit_breaker_max_attempts(value)
|
112
|
+
attempts = Integer(value)
|
113
|
+
raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
|
114
|
+
|
115
|
+
attempts
|
116
|
+
end
|
117
|
+
|
118
|
+
def option_circuit_breaker_reset_attempts_in(value)
|
119
|
+
timeout = Float(value)
|
120
|
+
raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
|
121
|
+
|
122
|
+
timeout
|
123
|
+
end
|
124
|
+
|
125
|
+
def option_circuit_breaker_break_in(value)
|
126
|
+
timeout = Float(value)
|
127
|
+
raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
|
128
|
+
|
129
|
+
timeout
|
130
|
+
end
|
131
|
+
|
132
|
+
def option_circuit_breaker_half_open_drip_rate(value)
|
133
|
+
ratio = Float(value)
|
134
|
+
raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
|
135
|
+
|
136
|
+
ratio
|
137
|
+
end
|
138
|
+
|
139
|
+
def option_circuit_breaker_break_on(value)
|
140
|
+
raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
|
141
|
+
|
142
|
+
value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
register_plugin :circuit_breaker, CircuitBreaker
|
147
|
+
end
|
148
|
+
end
|
@@ -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(*)
|
@@ -40,9 +40,15 @@ module HTTPX
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
def build_request(*)
|
44
|
+
request = super
|
45
|
+
request.headers.set_cookie(request.options.cookies[request.uri])
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
43
49
|
private
|
44
50
|
|
45
|
-
def on_response(
|
51
|
+
def on_response(_request, response)
|
46
52
|
if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
|
47
53
|
|
48
54
|
log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
|
@@ -52,12 +58,6 @@ module HTTPX
|
|
52
58
|
|
53
59
|
super
|
54
60
|
end
|
55
|
-
|
56
|
-
def build_request(*, _)
|
57
|
-
request = super
|
58
|
-
request.headers.set_cookie(request.options.cookies[request.uri])
|
59
|
-
request
|
60
|
-
end
|
61
61
|
end
|
62
62
|
|
63
63
|
module HeadersMethods
|
@@ -70,23 +70,36 @@ module HTTPX
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
+
# adds support for the following options:
|
74
|
+
#
|
75
|
+
# :cookies :: cookie jar for the session (can be a Hash, an Array, an instance of HTTPX::Plugins::Cookies::CookieJar)
|
73
76
|
module OptionsMethods
|
74
|
-
def
|
75
|
-
super
|
77
|
+
def option_headers(*)
|
78
|
+
value = super
|
76
79
|
|
77
|
-
|
80
|
+
merge_cookie_in_jar(value.delete("cookie"), @cookies) if defined?(@cookies) && value.key?("cookie")
|
78
81
|
|
79
|
-
|
82
|
+
value
|
83
|
+
end
|
84
|
+
|
85
|
+
def option_cookies(value)
|
86
|
+
jar = value.is_a?(Jar) ? value : Jar.new(value)
|
87
|
+
|
88
|
+
merge_cookie_in_jar(@headers.delete("cookie"), jar) if defined?(@headers) && @headers.key?("cookie")
|
89
|
+
|
90
|
+
jar
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def merge_cookie_in_jar(cookies, jar)
|
96
|
+
cookies.each do |ck|
|
80
97
|
ck.split(/ *; */).each do |cookie|
|
81
98
|
name, value = cookie.split("=", 2)
|
82
|
-
|
99
|
+
jar.add(Cookie.new(name, value))
|
83
100
|
end
|
84
101
|
end
|
85
102
|
end
|
86
|
-
|
87
|
-
def option_cookies(value)
|
88
|
-
value.is_a?(Jar) ? value : Jar.new(value)
|
89
|
-
end
|
90
103
|
end
|
91
104
|
end
|
92
105
|
register_plugin :cookies, Cookies
|