httpx-patched 1.6.2.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 +7 -0
- data/LICENSE.txt +191 -0
- data/README.md +162 -0
- data/doc/release_notes/0_0_1.md +7 -0
- data/doc/release_notes/0_0_2.md +9 -0
- data/doc/release_notes/0_0_3.md +9 -0
- data/doc/release_notes/0_0_4.md +7 -0
- data/doc/release_notes/0_0_5.md +5 -0
- data/doc/release_notes/0_10_0.md +66 -0
- data/doc/release_notes/0_10_1.md +37 -0
- data/doc/release_notes/0_10_2.md +5 -0
- data/doc/release_notes/0_11_0.md +74 -0
- data/doc/release_notes/0_11_1.md +5 -0
- data/doc/release_notes/0_11_2.md +5 -0
- data/doc/release_notes/0_11_3.md +5 -0
- data/doc/release_notes/0_12_0.md +55 -0
- data/doc/release_notes/0_13_0.md +58 -0
- data/doc/release_notes/0_13_1.md +5 -0
- data/doc/release_notes/0_13_2.md +9 -0
- data/doc/release_notes/0_14_0.md +79 -0
- data/doc/release_notes/0_14_1.md +7 -0
- data/doc/release_notes/0_14_2.md +6 -0
- data/doc/release_notes/0_14_3.md +5 -0
- data/doc/release_notes/0_14_4.md +5 -0
- data/doc/release_notes/0_14_5.md +11 -0
- data/doc/release_notes/0_15_0.md +53 -0
- data/doc/release_notes/0_15_1.md +8 -0
- data/doc/release_notes/0_15_2.md +9 -0
- data/doc/release_notes/0_15_3.md +5 -0
- data/doc/release_notes/0_15_4.md +5 -0
- data/doc/release_notes/0_16_0.md +93 -0
- data/doc/release_notes/0_16_1.md +5 -0
- data/doc/release_notes/0_17_0.md +49 -0
- data/doc/release_notes/0_18_0.md +69 -0
- data/doc/release_notes/0_18_1.md +12 -0
- data/doc/release_notes/0_18_2.md +10 -0
- data/doc/release_notes/0_18_3.md +7 -0
- data/doc/release_notes/0_18_4.md +14 -0
- data/doc/release_notes/0_18_5.md +10 -0
- data/doc/release_notes/0_18_6.md +5 -0
- data/doc/release_notes/0_18_7.md +5 -0
- data/doc/release_notes/0_19_0.md +39 -0
- data/doc/release_notes/0_19_1.md +5 -0
- data/doc/release_notes/0_19_2.md +7 -0
- data/doc/release_notes/0_19_3.md +6 -0
- data/doc/release_notes/0_19_4.md +14 -0
- data/doc/release_notes/0_19_5.md +13 -0
- data/doc/release_notes/0_19_6.md +5 -0
- data/doc/release_notes/0_19_7.md +5 -0
- data/doc/release_notes/0_19_8.md +5 -0
- data/doc/release_notes/0_1_0.md +9 -0
- data/doc/release_notes/0_20_0.md +36 -0
- 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/0_2_0.md +5 -0
- data/doc/release_notes/0_2_1.md +16 -0
- data/doc/release_notes/0_3_0.md +12 -0
- data/doc/release_notes/0_3_1.md +6 -0
- data/doc/release_notes/0_4_0.md +51 -0
- data/doc/release_notes/0_4_1.md +3 -0
- data/doc/release_notes/0_5_0.md +15 -0
- data/doc/release_notes/0_5_1.md +14 -0
- data/doc/release_notes/0_6_0.md +5 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_6_2.md +6 -0
- data/doc/release_notes/0_6_3.md +13 -0
- data/doc/release_notes/0_6_4.md +21 -0
- data/doc/release_notes/0_6_5.md +22 -0
- data/doc/release_notes/0_6_6.md +19 -0
- data/doc/release_notes/0_6_7.md +5 -0
- data/doc/release_notes/0_7_0.md +46 -0
- data/doc/release_notes/0_8_0.md +27 -0
- data/doc/release_notes/0_8_1.md +8 -0
- data/doc/release_notes/0_8_2.md +7 -0
- data/doc/release_notes/0_9_0.md +38 -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/doc/release_notes/1_3_2.md +6 -0
- data/doc/release_notes/1_3_3.md +5 -0
- data/doc/release_notes/1_3_4.md +6 -0
- data/doc/release_notes/1_4_0.md +43 -0
- data/doc/release_notes/1_4_1.md +19 -0
- data/doc/release_notes/1_4_2.md +20 -0
- data/doc/release_notes/1_4_3.md +11 -0
- data/doc/release_notes/1_4_4.md +14 -0
- data/doc/release_notes/1_5_0.md +126 -0
- data/doc/release_notes/1_5_1.md +6 -0
- data/doc/release_notes/1_6_0.md +50 -0
- data/doc/release_notes/1_6_1.md +17 -0
- data/doc/release_notes/1_6_2.md +11 -0
- data/lib/httpx/adapters/datadog.rb +359 -0
- data/lib/httpx/adapters/faraday.rb +303 -0
- data/lib/httpx/adapters/sentry.rb +121 -0
- data/lib/httpx/adapters/webmock.rb +175 -0
- data/lib/httpx/altsvc.rb +163 -0
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/buffer.rb +61 -0
- data/lib/httpx/callbacks.rb +35 -0
- data/lib/httpx/chainable.rb +106 -0
- data/lib/httpx/connection/http1.rb +399 -0
- data/lib/httpx/connection/http2.rb +468 -0
- data/lib/httpx/connection.rb +954 -0
- data/lib/httpx/domain_name.rb +145 -0
- data/lib/httpx/errors.rb +111 -0
- data/lib/httpx/extensions.rb +59 -0
- data/lib/httpx/headers.rb +176 -0
- data/lib/httpx/io/ssl.rb +163 -0
- data/lib/httpx/io/tcp.rb +239 -0
- data/lib/httpx/io/udp.rb +62 -0
- data/lib/httpx/io/unix.rb +71 -0
- data/lib/httpx/io.rb +11 -0
- data/lib/httpx/loggable.rb +56 -0
- data/lib/httpx/options.rb +463 -0
- data/lib/httpx/parser/http1.rb +186 -0
- data/lib/httpx/plugins/auth/basic.rb +20 -0
- data/lib/httpx/plugins/auth/digest.rb +102 -0
- data/lib/httpx/plugins/auth/ntlm.rb +35 -0
- data/lib/httpx/plugins/auth/socks5.rb +22 -0
- data/lib/httpx/plugins/auth.rb +25 -0
- data/lib/httpx/plugins/aws_sdk_authentication.rb +111 -0
- data/lib/httpx/plugins/aws_sigv4.rb +239 -0
- data/lib/httpx/plugins/basic_auth.rb +29 -0
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/callbacks.rb +127 -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 +147 -0
- data/lib/httpx/plugins/content_digest.rb +204 -0
- data/lib/httpx/plugins/cookies/cookie.rb +174 -0
- data/lib/httpx/plugins/cookies/jar.rb +95 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +143 -0
- data/lib/httpx/plugins/cookies.rb +107 -0
- data/lib/httpx/plugins/digest_auth.rb +67 -0
- data/lib/httpx/plugins/expect.rb +120 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
- data/lib/httpx/plugins/follow_redirects.rb +233 -0
- data/lib/httpx/plugins/grpc/call.rb +63 -0
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +90 -0
- data/lib/httpx/plugins/grpc/message.rb +55 -0
- data/lib/httpx/plugins/grpc.rb +282 -0
- data/lib/httpx/plugins/h2c.rb +127 -0
- data/lib/httpx/plugins/internal_telemetry.rb +107 -0
- data/lib/httpx/plugins/ntlm_auth.rb +62 -0
- data/lib/httpx/plugins/oauth.rb +183 -0
- data/lib/httpx/plugins/persistent.rb +82 -0
- data/lib/httpx/plugins/proxy/http.rb +184 -0
- data/lib/httpx/plugins/proxy/socks4.rb +135 -0
- data/lib/httpx/plugins/proxy/socks5.rb +194 -0
- data/lib/httpx/plugins/proxy/ssh.rb +94 -0
- data/lib/httpx/plugins/proxy.rb +349 -0
- data/lib/httpx/plugins/push_promise.rb +81 -0
- data/lib/httpx/plugins/query.rb +35 -0
- data/lib/httpx/plugins/rate_limiter.rb +55 -0
- data/lib/httpx/plugins/response_cache/file_store.rb +140 -0
- data/lib/httpx/plugins/response_cache/store.rb +33 -0
- data/lib/httpx/plugins/response_cache.rb +333 -0
- data/lib/httpx/plugins/retries.rb +230 -0
- data/lib/httpx/plugins/ssrf_filter.rb +145 -0
- data/lib/httpx/plugins/stream.rb +183 -0
- data/lib/httpx/plugins/stream_bidi.rb +315 -0
- data/lib/httpx/plugins/upgrade/h2.rb +64 -0
- data/lib/httpx/plugins/upgrade.rb +86 -0
- data/lib/httpx/plugins/webdav.rb +86 -0
- data/lib/httpx/plugins/xml.rb +76 -0
- data/lib/httpx/pmatch_extensions.rb +33 -0
- data/lib/httpx/pool.rb +190 -0
- data/lib/httpx/punycode.rb +22 -0
- data/lib/httpx/request/body.rb +158 -0
- data/lib/httpx/request.rb +328 -0
- data/lib/httpx/resolver/entry.rb +30 -0
- data/lib/httpx/resolver/https.rb +256 -0
- data/lib/httpx/resolver/multi.rb +102 -0
- data/lib/httpx/resolver/native.rb +547 -0
- data/lib/httpx/resolver/resolver.rb +173 -0
- data/lib/httpx/resolver/system.rb +255 -0
- data/lib/httpx/resolver.rb +189 -0
- data/lib/httpx/response/body.rb +242 -0
- data/lib/httpx/response/buffer.rb +115 -0
- data/lib/httpx/response.rb +304 -0
- data/lib/httpx/selector.rb +282 -0
- data/lib/httpx/session.rb +612 -0
- data/lib/httpx/session_extensions.rb +30 -0
- data/lib/httpx/timers.rb +133 -0
- data/lib/httpx/transcoder/body.rb +43 -0
- data/lib/httpx/transcoder/chunker.rb +115 -0
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +68 -0
- data/lib/httpx/transcoder/gzip.rb +71 -0
- data/lib/httpx/transcoder/json.rb +71 -0
- data/lib/httpx/transcoder/multipart/decoder.rb +141 -0
- data/lib/httpx/transcoder/multipart/encoder.rb +120 -0
- data/lib/httpx/transcoder/multipart/mime_type_detector.rb +78 -0
- data/lib/httpx/transcoder/multipart/part.rb +35 -0
- data/lib/httpx/transcoder/multipart.rb +31 -0
- data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
- data/lib/httpx/transcoder/utils/deflater.rb +75 -0
- data/lib/httpx/transcoder.rb +91 -0
- data/lib/httpx/utils.rb +75 -0
- data/lib/httpx/version.rb +5 -0
- data/lib/httpx.rb +66 -0
- data/sig/altsvc.rbs +33 -0
- data/sig/buffer.rbs +27 -0
- data/sig/callbacks.rbs +15 -0
- data/sig/chainable.rbs +55 -0
- data/sig/connection/http1.rbs +85 -0
- data/sig/connection/http2.rbs +116 -0
- data/sig/connection.rbs +169 -0
- data/sig/domain_name.rbs +17 -0
- data/sig/errors.rbs +69 -0
- data/sig/headers.rbs +49 -0
- data/sig/httpx.rbs +27 -0
- data/sig/io/ssl.rbs +27 -0
- data/sig/io/tcp.rbs +72 -0
- data/sig/io/udp.rbs +25 -0
- data/sig/io/unix.rbs +26 -0
- data/sig/io.rbs +3 -0
- data/sig/loggable.rbs +17 -0
- data/sig/options.rbs +202 -0
- data/sig/parser/http1.rbs +59 -0
- data/sig/plugins/auth/basic.rbs +17 -0
- data/sig/plugins/auth/digest.rbs +25 -0
- data/sig/plugins/auth/ntlm.rbs +20 -0
- data/sig/plugins/auth/socks5.rbs +18 -0
- data/sig/plugins/auth.rbs +13 -0
- data/sig/plugins/aws_sdk_authentication.rbs +43 -0
- data/sig/plugins/aws_sigv4.rbs +78 -0
- data/sig/plugins/basic_auth.rbs +15 -0
- 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 +57 -0
- data/sig/plugins/content_digest.rbs +51 -0
- data/sig/plugins/cookies/cookie.rbs +55 -0
- data/sig/plugins/cookies/jar.rbs +26 -0
- data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
- data/sig/plugins/cookies.rbs +28 -0
- data/sig/plugins/digest_auth.rbs +21 -0
- data/sig/plugins/expect.rbs +15 -0
- data/sig/plugins/fiber_concurrency.rbs +51 -0
- data/sig/plugins/follow_redirects.rbs +47 -0
- data/sig/plugins/grpc/call.rbs +23 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +65 -0
- data/sig/plugins/h2c.rbs +27 -0
- data/sig/plugins/ntlm_auth.rbs +21 -0
- data/sig/plugins/oauth.rbs +68 -0
- data/sig/plugins/persistent.rbs +14 -0
- data/sig/plugins/proxy/http.rbs +30 -0
- data/sig/plugins/proxy/socks4.rbs +37 -0
- data/sig/plugins/proxy/socks5.rbs +49 -0
- data/sig/plugins/proxy/ssh.rbs +18 -0
- data/sig/plugins/proxy.rbs +70 -0
- data/sig/plugins/push_promise.rbs +23 -0
- data/sig/plugins/query.rbs +18 -0
- data/sig/plugins/rate_limiter.rbs +13 -0
- data/sig/plugins/response_cache/file_store.rbs +19 -0
- data/sig/plugins/response_cache/store.rbs +13 -0
- data/sig/plugins/response_cache.rbs +86 -0
- data/sig/plugins/retries.rbs +66 -0
- data/sig/plugins/ssrf_filter.rbs +26 -0
- data/sig/plugins/stream.rbs +54 -0
- data/sig/plugins/stream_bidi.rbs +68 -0
- data/sig/plugins/upgrade/h2.rbs +9 -0
- data/sig/plugins/upgrade.rbs +29 -0
- data/sig/plugins/webdav.rbs +23 -0
- data/sig/plugins/xml.rbs +37 -0
- data/sig/pool.rbs +51 -0
- data/sig/punycode.rbs +5 -0
- data/sig/request/body.rbs +34 -0
- data/sig/request.rbs +88 -0
- data/sig/resolver/entry.rbs +13 -0
- data/sig/resolver/https.rbs +45 -0
- data/sig/resolver/multi.rbs +32 -0
- data/sig/resolver/native.rbs +74 -0
- data/sig/resolver/resolver.rbs +64 -0
- data/sig/resolver/system.rbs +34 -0
- data/sig/resolver.rbs +48 -0
- data/sig/response/body.rbs +52 -0
- data/sig/response/buffer.rbs +23 -0
- data/sig/response.rbs +103 -0
- data/sig/selector.rbs +68 -0
- data/sig/session.rbs +104 -0
- data/sig/timers.rbs +54 -0
- data/sig/transcoder/body.rbs +24 -0
- data/sig/transcoder/chunker.rbs +49 -0
- data/sig/transcoder/deflate.rbs +12 -0
- data/sig/transcoder/form.rbs +34 -0
- data/sig/transcoder/gzip.rbs +27 -0
- data/sig/transcoder/json.rbs +28 -0
- data/sig/transcoder/multipart.rbs +103 -0
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +28 -0
- data/sig/transcoder.rbs +43 -0
- data/sig/utils.rbs +19 -0
- metadata +518 -0
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
CALLBACKS = %i[
|
|
12
|
+
connection_opened connection_closed
|
|
13
|
+
request_error
|
|
14
|
+
request_started request_body_chunk request_completed
|
|
15
|
+
response_started response_body_chunk response_completed
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# connection closed user-space errors happen after errors can be surfaced to requests,
|
|
19
|
+
# so they need to pierce through the scheduler, which is only possible by simulating an
|
|
20
|
+
# interrupt.
|
|
21
|
+
class CallbackError < Exception; end # rubocop:disable Lint/InheritException
|
|
22
|
+
|
|
23
|
+
module InstanceMethods
|
|
24
|
+
include HTTPX::Callbacks
|
|
25
|
+
|
|
26
|
+
CALLBACKS.each do |meth|
|
|
27
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
|
28
|
+
def on_#{meth}(&blk) # def on_connection_opened(&blk)
|
|
29
|
+
on(:#{meth}, &blk) # on(:connection_opened, &blk)
|
|
30
|
+
self # self
|
|
31
|
+
end # end
|
|
32
|
+
MOD
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def plugin(*args, &blk)
|
|
36
|
+
super(*args).tap do |sess|
|
|
37
|
+
CALLBACKS.each do |cb|
|
|
38
|
+
next unless callbacks_for?(cb)
|
|
39
|
+
|
|
40
|
+
sess.callbacks(cb).concat(callbacks(cb))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sess.wrap(&blk) if blk
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def branch(options, &blk)
|
|
50
|
+
super(options).tap do |sess|
|
|
51
|
+
CALLBACKS.each do |cb|
|
|
52
|
+
next unless callbacks_for?(cb)
|
|
53
|
+
|
|
54
|
+
sess.callbacks(cb).concat(callbacks(cb))
|
|
55
|
+
end
|
|
56
|
+
sess.wrap(&blk) if blk
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def do_init_connection(connection, selector)
|
|
61
|
+
super
|
|
62
|
+
connection.on(:open) do
|
|
63
|
+
next unless connection.current_session == self
|
|
64
|
+
|
|
65
|
+
emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
|
|
66
|
+
end
|
|
67
|
+
connection.on(:close) do
|
|
68
|
+
next unless connection.current_session == self
|
|
69
|
+
|
|
70
|
+
emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
connection
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set_request_callbacks(request)
|
|
77
|
+
super
|
|
78
|
+
|
|
79
|
+
request.on(:headers) do
|
|
80
|
+
emit_or_callback_error(:request_started, request)
|
|
81
|
+
end
|
|
82
|
+
request.on(:body_chunk) do |chunk|
|
|
83
|
+
emit_or_callback_error(:request_body_chunk, request, chunk)
|
|
84
|
+
end
|
|
85
|
+
request.on(:done) do
|
|
86
|
+
emit_or_callback_error(:request_completed, request)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
request.on(:response_started) do |res|
|
|
90
|
+
if res.is_a?(Response)
|
|
91
|
+
emit_or_callback_error(:response_started, request, res)
|
|
92
|
+
res.on(:chunk_received) do |chunk|
|
|
93
|
+
emit_or_callback_error(:response_body_chunk, request, res, chunk)
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
emit_or_callback_error(:request_error, request, res.error)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
request.on(:response) do |res|
|
|
100
|
+
emit_or_callback_error(:response_completed, request, res) if res.is_a?(Response)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def emit_or_callback_error(*args)
|
|
105
|
+
emit(*args)
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
ex = CallbackError.new(e.message)
|
|
108
|
+
ex.set_backtrace(e.backtrace)
|
|
109
|
+
raise ex
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def receive_requests(*)
|
|
113
|
+
super
|
|
114
|
+
rescue CallbackError => e
|
|
115
|
+
raise e.cause
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def close(*)
|
|
119
|
+
super
|
|
120
|
+
rescue CallbackError => e
|
|
121
|
+
raise e.cause
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
register_plugin :callbacks, Callbacks
|
|
126
|
+
end
|
|
127
|
+
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,147 @@
|
|
|
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
|
+
%i[circuit_open].each do |meth|
|
|
36
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
|
37
|
+
def on_#{meth}(&blk) # def on_circuit_open(&blk)
|
|
38
|
+
on(:#{meth}, &blk) # on(:circuit_open, &blk)
|
|
39
|
+
self # self
|
|
40
|
+
end # end
|
|
41
|
+
MOD
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def send_requests(*requests)
|
|
47
|
+
# @type var short_circuit_responses: Array[response]
|
|
48
|
+
short_circuit_responses = []
|
|
49
|
+
|
|
50
|
+
# run all requests through the circuit breaker, see if the circuit is
|
|
51
|
+
# open for any of them.
|
|
52
|
+
real_requests = requests.each_with_index.with_object([]) do |(req, idx), real_reqs|
|
|
53
|
+
short_circuit_response = @circuit_store.try_respond(req)
|
|
54
|
+
if short_circuit_response.nil?
|
|
55
|
+
real_reqs << req
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
short_circuit_responses[idx] = short_circuit_response
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# run requests for the remainder
|
|
62
|
+
unless real_requests.empty?
|
|
63
|
+
responses = super(*real_requests)
|
|
64
|
+
|
|
65
|
+
real_requests.each_with_index do |request, idx|
|
|
66
|
+
short_circuit_responses[requests.index(request)] = responses[idx]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
short_circuit_responses
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def set_request_callbacks(request)
|
|
74
|
+
super
|
|
75
|
+
request.on(:response) do |response|
|
|
76
|
+
emit(:circuit_open, request) if try_circuit_open(request, response)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def try_circuit_open(request, response)
|
|
81
|
+
if response.is_a?(ErrorResponse)
|
|
82
|
+
case response.error
|
|
83
|
+
when RequestTimeoutError
|
|
84
|
+
@circuit_store.try_open(request.uri, response)
|
|
85
|
+
else
|
|
86
|
+
@circuit_store.try_open(request.origin, response)
|
|
87
|
+
end
|
|
88
|
+
elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
|
|
89
|
+
@circuit_store.try_open(request.uri, response)
|
|
90
|
+
else
|
|
91
|
+
@circuit_store.try_close(request.uri)
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# adds support for the following options:
|
|
98
|
+
#
|
|
99
|
+
# :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
|
|
100
|
+
# :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
|
|
101
|
+
# :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
|
|
102
|
+
# (i.e. <tt>->(res) { res.status == 429 } </tt>)
|
|
103
|
+
# :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
|
|
104
|
+
# (defaults to <tt><60</tt>).
|
|
105
|
+
# :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
|
|
106
|
+
# (defaults to <tt>1</tt>).
|
|
107
|
+
module OptionsMethods
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def option_circuit_breaker_max_attempts(value)
|
|
111
|
+
attempts = Integer(value)
|
|
112
|
+
raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
|
|
113
|
+
|
|
114
|
+
attempts
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def option_circuit_breaker_reset_attempts_in(value)
|
|
118
|
+
timeout = Float(value)
|
|
119
|
+
raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
|
|
120
|
+
|
|
121
|
+
timeout
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def option_circuit_breaker_break_in(value)
|
|
125
|
+
timeout = Float(value)
|
|
126
|
+
raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
|
|
127
|
+
|
|
128
|
+
timeout
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def option_circuit_breaker_half_open_drip_rate(value)
|
|
132
|
+
ratio = Float(value)
|
|
133
|
+
raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
|
|
134
|
+
|
|
135
|
+
ratio
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def option_circuit_breaker_break_on(value)
|
|
139
|
+
raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
|
|
140
|
+
|
|
141
|
+
value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
register_plugin :circuit_breaker, CircuitBreaker
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds `Content-Digest` headers to requests
|
|
7
|
+
# and can validate these headers on responses
|
|
8
|
+
#
|
|
9
|
+
# https://datatracker.ietf.org/doc/html/rfc9530
|
|
10
|
+
#
|
|
11
|
+
module ContentDigest
|
|
12
|
+
class Error < HTTPX::Error; end
|
|
13
|
+
|
|
14
|
+
# Error raised on response "content-digest" header validation.
|
|
15
|
+
class ValidationError < Error
|
|
16
|
+
attr_reader :response
|
|
17
|
+
|
|
18
|
+
def initialize(message, response)
|
|
19
|
+
super(message)
|
|
20
|
+
@response = response
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class MissingContentDigestError < ValidationError; end
|
|
25
|
+
class InvalidContentDigestError < ValidationError; end
|
|
26
|
+
|
|
27
|
+
SUPPORTED_ALGORITHMS = {
|
|
28
|
+
"sha-256" => OpenSSL::Digest::SHA256,
|
|
29
|
+
"sha-512" => OpenSSL::Digest::SHA512,
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def extra_options(options)
|
|
34
|
+
options.merge(encode_content_digest: true, validate_content_digest: false, content_digest_algorithm: "sha-256")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# add support for the following options:
|
|
39
|
+
#
|
|
40
|
+
# :content_digest_algorithm :: the digest algorithm to use. Currently supports `sha-256` and `sha-512`. (defaults to `sha-256`)
|
|
41
|
+
# :encode_content_digest :: whether a <tt>Content-Digest</tt> header should be computed for the request;
|
|
42
|
+
# can also be a callable object (i.e. <tt>->(req) { ... }</tt>, defaults to <tt>true</tt>)
|
|
43
|
+
# :validate_content_digest :: whether a <tt>Content-Digest</tt> header in the response should be validated;
|
|
44
|
+
# can also be a callable object (i.e. <tt>->(res) { ... }</tt>, defaults to <tt>false</tt>)
|
|
45
|
+
module OptionsMethods
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def option_content_digest_algorithm(value)
|
|
49
|
+
raise TypeError, ":content_digest_algorithm must be one of 'sha-256', 'sha-512'" unless SUPPORTED_ALGORITHMS.key?(value)
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def option_encode_content_digest(value)
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def option_validate_content_digest(value)
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module ResponseBodyMethods
|
|
64
|
+
attr_reader :content_digest_buffer
|
|
65
|
+
|
|
66
|
+
def initialize(response, options)
|
|
67
|
+
super
|
|
68
|
+
|
|
69
|
+
return unless response.headers.key?("content-digest")
|
|
70
|
+
|
|
71
|
+
should_validate = options.validate_content_digest
|
|
72
|
+
should_validate = should_validate.call(response) if should_validate.respond_to?(:call)
|
|
73
|
+
|
|
74
|
+
return unless should_validate
|
|
75
|
+
|
|
76
|
+
@content_digest_buffer = Response::Buffer.new(
|
|
77
|
+
threshold_size: @options.body_threshold_size,
|
|
78
|
+
bytesize: @length,
|
|
79
|
+
encoding: @encoding
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def write(chunk)
|
|
84
|
+
@content_digest_buffer.write(chunk) if @content_digest_buffer
|
|
85
|
+
super
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def close
|
|
89
|
+
if @content_digest_buffer
|
|
90
|
+
@content_digest_buffer.close
|
|
91
|
+
@content_digest_buffer = nil
|
|
92
|
+
end
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
module InstanceMethods
|
|
98
|
+
def build_request(*)
|
|
99
|
+
request = super
|
|
100
|
+
|
|
101
|
+
return request if request.empty?
|
|
102
|
+
|
|
103
|
+
return request if request.headers.key?("content-digest")
|
|
104
|
+
|
|
105
|
+
perform_encoding = @options.encode_content_digest
|
|
106
|
+
perform_encoding = perform_encoding.call(request) if perform_encoding.respond_to?(:call)
|
|
107
|
+
|
|
108
|
+
return request unless perform_encoding
|
|
109
|
+
|
|
110
|
+
digest = base64digest(request.body)
|
|
111
|
+
request.headers.add("content-digest", "#{@options.content_digest_algorithm}=:#{digest}:")
|
|
112
|
+
|
|
113
|
+
request
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def fetch_response(request, _, _)
|
|
119
|
+
response = super
|
|
120
|
+
return response unless response.is_a?(Response)
|
|
121
|
+
|
|
122
|
+
perform_validation = @options.validate_content_digest
|
|
123
|
+
perform_validation = perform_validation.call(response) if perform_validation.respond_to?(:call)
|
|
124
|
+
|
|
125
|
+
validate_content_digest(response) if perform_validation
|
|
126
|
+
|
|
127
|
+
response
|
|
128
|
+
rescue ValidationError => e
|
|
129
|
+
ErrorResponse.new(request, e)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_content_digest(response)
|
|
133
|
+
content_digest_header = response.headers["content-digest"]
|
|
134
|
+
|
|
135
|
+
raise MissingContentDigestError.new("response is missing a `content-digest` header", response) unless content_digest_header
|
|
136
|
+
|
|
137
|
+
digests = extract_content_digests(content_digest_header)
|
|
138
|
+
|
|
139
|
+
included_algorithms = SUPPORTED_ALGORITHMS.keys & digests.keys
|
|
140
|
+
|
|
141
|
+
raise MissingContentDigestError.new("unsupported algorithms: #{digests.keys.join(", ")}", response) if included_algorithms.empty?
|
|
142
|
+
|
|
143
|
+
content_buffer = response.body.content_digest_buffer
|
|
144
|
+
|
|
145
|
+
included_algorithms.each do |algorithm|
|
|
146
|
+
digest = SUPPORTED_ALGORITHMS.fetch(algorithm).new
|
|
147
|
+
digest_received = digests[algorithm]
|
|
148
|
+
digest_computed =
|
|
149
|
+
if content_buffer.respond_to?(:to_path)
|
|
150
|
+
content_buffer.flush
|
|
151
|
+
digest.file(content_buffer.to_path).base64digest
|
|
152
|
+
else
|
|
153
|
+
digest.base64digest(content_buffer.to_s)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
raise InvalidContentDigestError.new("#{algorithm} digest does not match content",
|
|
157
|
+
response) unless digest_received == digest_computed
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def extract_content_digests(header)
|
|
162
|
+
header.split(",").to_h do |entry|
|
|
163
|
+
algorithm, digest = entry.split("=", 2)
|
|
164
|
+
raise Error, "#{entry} is an invalid digest format" unless algorithm && digest
|
|
165
|
+
|
|
166
|
+
[algorithm, digest.byteslice(1..-2)]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def base64digest(body)
|
|
171
|
+
digest = SUPPORTED_ALGORITHMS.fetch(@options.content_digest_algorithm).new
|
|
172
|
+
|
|
173
|
+
if body.respond_to?(:read)
|
|
174
|
+
if body.respond_to?(:to_path)
|
|
175
|
+
digest.file(body.to_path).base64digest
|
|
176
|
+
else
|
|
177
|
+
raise ContentDigestError, "request body must be rewindable" unless body.respond_to?(:rewind)
|
|
178
|
+
|
|
179
|
+
buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
180
|
+
begin
|
|
181
|
+
IO.copy_stream(body, buffer)
|
|
182
|
+
buffer.flush
|
|
183
|
+
|
|
184
|
+
digest.file(buffer.to_path).base64digest
|
|
185
|
+
ensure
|
|
186
|
+
body.rewind
|
|
187
|
+
buffer.close
|
|
188
|
+
buffer.unlink
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
raise ContentDigestError, "base64digest for endless enumerators is not supported" if body.unbounded_body?
|
|
193
|
+
|
|
194
|
+
buffer = "".b
|
|
195
|
+
body.each { |chunk| buffer << chunk }
|
|
196
|
+
|
|
197
|
+
digest.base64digest(buffer)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
register_plugin :content_digest, ContentDigest
|
|
203
|
+
end
|
|
204
|
+
end
|