httpx 0.20.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|