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,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for using the experimental QUERY HTTP method
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Query
|
|
9
|
+
module Query
|
|
10
|
+
def self.subplugins
|
|
11
|
+
{
|
|
12
|
+
retries: QueryRetries,
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module InstanceMethods
|
|
17
|
+
def query(*uri, **options)
|
|
18
|
+
request("QUERY", uri, **options)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module QueryRetries
|
|
23
|
+
module InstanceMethods
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def repeatable_request?(request, options)
|
|
27
|
+
super || request.verb == "QUERY"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
register_plugin :query, Query
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for retrying requests when the request:
|
|
7
|
+
#
|
|
8
|
+
# * is rate limited;
|
|
9
|
+
# * when the server is unavailable (503);
|
|
10
|
+
# * when a 3xx request comes with a "retry-after" value
|
|
11
|
+
#
|
|
12
|
+
# https://gitlab.com/os85/httpx/wikis/Rate-Limiter
|
|
13
|
+
#
|
|
14
|
+
module RateLimiter
|
|
15
|
+
class << self
|
|
16
|
+
RATE_LIMIT_CODES = [429, 503].freeze
|
|
17
|
+
|
|
18
|
+
def configure(klass)
|
|
19
|
+
klass.plugin(:retries,
|
|
20
|
+
retry_change_requests: true,
|
|
21
|
+
retry_on: method(:retry_on_rate_limited_response),
|
|
22
|
+
retry_after: method(:retry_after_rate_limit))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retry_on_rate_limited_response(response)
|
|
26
|
+
return false unless response.is_a?(Response)
|
|
27
|
+
|
|
28
|
+
status = response.status
|
|
29
|
+
|
|
30
|
+
RATE_LIMIT_CODES.include?(status)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Servers send the "Retry-After" header field to indicate how long the
|
|
34
|
+
# user agent ought to wait before making a follow-up request. When
|
|
35
|
+
# sent with a 503 (Service Unavailable) response, Retry-After indicates
|
|
36
|
+
# how long the service is expected to be unavailable to the client.
|
|
37
|
+
# When sent with any 3xx (Redirection) response, Retry-After indicates
|
|
38
|
+
# the minimum time that the user agent is asked to wait before issuing
|
|
39
|
+
# the redirected request.
|
|
40
|
+
#
|
|
41
|
+
def retry_after_rate_limit(_, response)
|
|
42
|
+
return unless response.is_a?(Response)
|
|
43
|
+
|
|
44
|
+
retry_after = response.headers["retry-after"]
|
|
45
|
+
|
|
46
|
+
return unless retry_after
|
|
47
|
+
|
|
48
|
+
Utils.parse_retry_after(retry_after)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
register_plugin :rate_limiter, RateLimiter
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module HTTPX::Plugins
|
|
6
|
+
module ResponseCache
|
|
7
|
+
# Implementation of a file system based cache store.
|
|
8
|
+
#
|
|
9
|
+
# It stores cached responses in a file under a directory pointed by the +dir+
|
|
10
|
+
# variable (defaults to the default temp directory from the OS), in a custom
|
|
11
|
+
# format (similar but different from HTTP/1.1 request/response framing).
|
|
12
|
+
class FileStore
|
|
13
|
+
CRLF = HTTPX::Connection::HTTP1::CRLF
|
|
14
|
+
|
|
15
|
+
attr_reader :dir
|
|
16
|
+
|
|
17
|
+
def initialize(dir = Dir.tmpdir)
|
|
18
|
+
@dir = Pathname.new(dir).join("httpx-response-cache")
|
|
19
|
+
|
|
20
|
+
FileUtils.mkdir_p(@dir)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clear
|
|
24
|
+
FileUtils.rm_rf(@dir)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get(request)
|
|
28
|
+
path = file_path(request)
|
|
29
|
+
|
|
30
|
+
return unless File.exist?(path)
|
|
31
|
+
|
|
32
|
+
File.open(path, mode: File::RDONLY | File::BINARY) do |f|
|
|
33
|
+
f.flock(File::Constants::LOCK_SH)
|
|
34
|
+
|
|
35
|
+
read_from_file(request, f)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set(request, response)
|
|
40
|
+
path = file_path(request)
|
|
41
|
+
|
|
42
|
+
file_exists = File.exist?(path)
|
|
43
|
+
|
|
44
|
+
mode = file_exists ? File::RDWR : File::CREAT | File::Constants::WRONLY
|
|
45
|
+
|
|
46
|
+
File.open(path, mode: mode | File::BINARY) do |f|
|
|
47
|
+
f.flock(File::Constants::LOCK_EX)
|
|
48
|
+
|
|
49
|
+
if file_exists
|
|
50
|
+
cached_response = read_from_file(request, f)
|
|
51
|
+
|
|
52
|
+
if cached_response
|
|
53
|
+
next if cached_response == request.cached_response
|
|
54
|
+
|
|
55
|
+
cached_response.close
|
|
56
|
+
|
|
57
|
+
f.truncate(0)
|
|
58
|
+
|
|
59
|
+
f.rewind
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# cache the request headers
|
|
63
|
+
f << request.verb << CRLF
|
|
64
|
+
f << request.uri << CRLF
|
|
65
|
+
|
|
66
|
+
request.headers.each do |field, value|
|
|
67
|
+
f << field << ":" << value << CRLF
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
f << CRLF
|
|
71
|
+
|
|
72
|
+
# cache the response
|
|
73
|
+
f << response.status << CRLF
|
|
74
|
+
f << response.version << CRLF
|
|
75
|
+
|
|
76
|
+
response.headers.each do |field, value|
|
|
77
|
+
f << field << ":" << value << CRLF
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
f << CRLF
|
|
81
|
+
|
|
82
|
+
response.body.rewind
|
|
83
|
+
|
|
84
|
+
IO.copy_stream(response.body, f)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def file_path(request)
|
|
91
|
+
@dir.join(request.response_cache_key)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_from_file(request, f)
|
|
95
|
+
# if it's an empty file
|
|
96
|
+
return if f.eof?
|
|
97
|
+
|
|
98
|
+
# read request data
|
|
99
|
+
verb = f.readline.delete_suffix!(CRLF)
|
|
100
|
+
uri = f.readline.delete_suffix!(CRLF)
|
|
101
|
+
|
|
102
|
+
request_headers = {}
|
|
103
|
+
while (line = f.readline) != CRLF
|
|
104
|
+
line.delete_suffix!(CRLF)
|
|
105
|
+
sep_index = line.index(":")
|
|
106
|
+
|
|
107
|
+
field = line.byteslice(0..(sep_index - 1))
|
|
108
|
+
value = line.byteslice((sep_index + 1)..-1)
|
|
109
|
+
|
|
110
|
+
request_headers[field] = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
status = f.readline.delete_suffix!(CRLF)
|
|
114
|
+
version = f.readline.delete_suffix!(CRLF)
|
|
115
|
+
|
|
116
|
+
response_headers = {}
|
|
117
|
+
while (line = f.readline) != CRLF
|
|
118
|
+
line.delete_suffix!(CRLF)
|
|
119
|
+
sep_index = line.index(":")
|
|
120
|
+
|
|
121
|
+
field = line.byteslice(0..(sep_index - 1))
|
|
122
|
+
value = line.byteslice((sep_index + 1)..-1)
|
|
123
|
+
|
|
124
|
+
response_headers[field] = value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
original_request = request.options.request_class.new(verb, uri, request.options)
|
|
128
|
+
original_request.merge_headers(request_headers)
|
|
129
|
+
|
|
130
|
+
response = request.options.response_class.new(request, status, version, response_headers)
|
|
131
|
+
response.original_request = original_request
|
|
132
|
+
response.finish!
|
|
133
|
+
|
|
134
|
+
IO.copy_stream(f, response.body)
|
|
135
|
+
|
|
136
|
+
response
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX::Plugins
|
|
4
|
+
module ResponseCache
|
|
5
|
+
# Implementation of a thread-safe in-memory cache store.
|
|
6
|
+
class Store
|
|
7
|
+
def initialize
|
|
8
|
+
@store = {}
|
|
9
|
+
@store_mutex = Thread::Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clear
|
|
13
|
+
@store_mutex.synchronize { @store.clear }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(request)
|
|
17
|
+
@store_mutex.synchronize do
|
|
18
|
+
@store[request.response_cache_key]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(request, response)
|
|
23
|
+
@store_mutex.synchronize do
|
|
24
|
+
cached_response = @store[request.response_cache_key]
|
|
25
|
+
|
|
26
|
+
cached_response.close if cached_response
|
|
27
|
+
|
|
28
|
+
@store[request.response_cache_key] = response
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for retrying requests when certain errors happen.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Response-Cache
|
|
9
|
+
#
|
|
10
|
+
module ResponseCache
|
|
11
|
+
CACHEABLE_VERBS = %w[GET HEAD].freeze
|
|
12
|
+
CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
|
|
13
|
+
SUPPORTED_VARY_HEADERS = %w[accept accept-encoding accept-language cookie origin].sort.freeze
|
|
14
|
+
private_constant :CACHEABLE_VERBS
|
|
15
|
+
private_constant :CACHEABLE_STATUS_CODES
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def load_dependencies(*)
|
|
19
|
+
require_relative "response_cache/store"
|
|
20
|
+
require_relative "response_cache/file_store"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# whether the +response+ can be stored in the response cache.
|
|
24
|
+
# (i.e. has a cacheable body, does not contain directives prohibiting storage, etc...)
|
|
25
|
+
def cacheable_response?(response)
|
|
26
|
+
response.is_a?(Response) &&
|
|
27
|
+
(
|
|
28
|
+
response.cache_control.nil? ||
|
|
29
|
+
# TODO: !response.cache_control.include?("private") && is shared cache
|
|
30
|
+
!response.cache_control.include?("no-store")
|
|
31
|
+
) &&
|
|
32
|
+
CACHEABLE_STATUS_CODES.include?(response.status) &&
|
|
33
|
+
# RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
|
|
34
|
+
# 410 MAY be stored by a cache and used in reply to a subsequent
|
|
35
|
+
# request, subject to the expiration mechanism, unless a cache-control
|
|
36
|
+
# directive prohibits caching. However, a cache that does not support
|
|
37
|
+
# the Range and Content-Range headers MUST NOT cache 206 (Partial
|
|
38
|
+
# Content) responses.
|
|
39
|
+
response.status != 206
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# whether the +response+
|
|
43
|
+
def not_modified?(response)
|
|
44
|
+
response.is_a?(Response) && response.status == 304
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extra_options(options)
|
|
48
|
+
options.merge(
|
|
49
|
+
supported_vary_headers: SUPPORTED_VARY_HEADERS,
|
|
50
|
+
response_cache_store: :store,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# adds support for the following options:
|
|
56
|
+
#
|
|
57
|
+
# :supported_vary_headers :: array of header values that will be considered for a "vary" header based cache validation
|
|
58
|
+
# (defaults to {SUPPORTED_VARY_HEADERS}).
|
|
59
|
+
# :response_cache_store :: object where cached responses are fetch from or stored in; defaults to <tt>:store</tt> (in-memory
|
|
60
|
+
# cache), can be set to <tt>:file_store</tt> (file system cache store) as well, or any object which
|
|
61
|
+
# abides by the Cache Store Interface
|
|
62
|
+
#
|
|
63
|
+
# The Cache Store Interface requires implementation of the following methods:
|
|
64
|
+
#
|
|
65
|
+
# * +#get(request) -> response or nil+
|
|
66
|
+
# * +#set(request, response) -> void+
|
|
67
|
+
# * +#clear() -> void+)
|
|
68
|
+
#
|
|
69
|
+
module OptionsMethods
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def option_response_cache_store(value)
|
|
73
|
+
case value
|
|
74
|
+
when :store
|
|
75
|
+
Store.new
|
|
76
|
+
when :file_store
|
|
77
|
+
FileStore.new
|
|
78
|
+
else
|
|
79
|
+
value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def option_supported_vary_headers(value)
|
|
84
|
+
Array(value).sort
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
module InstanceMethods
|
|
89
|
+
# wipes out all cached responses from the cache store.
|
|
90
|
+
def clear_response_cache
|
|
91
|
+
@options.response_cache_store.clear
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_request(*)
|
|
95
|
+
request = super
|
|
96
|
+
return request unless cacheable_request?(request)
|
|
97
|
+
|
|
98
|
+
prepare_cache(request)
|
|
99
|
+
|
|
100
|
+
request
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def send_request(request, *)
|
|
106
|
+
return request if request.response
|
|
107
|
+
|
|
108
|
+
super
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_response(request, *)
|
|
112
|
+
response = super
|
|
113
|
+
|
|
114
|
+
return unless response
|
|
115
|
+
|
|
116
|
+
if ResponseCache.not_modified?(response)
|
|
117
|
+
log { "returning cached response for #{request.uri}" }
|
|
118
|
+
|
|
119
|
+
response.copy_from_cached!
|
|
120
|
+
elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
|
|
121
|
+
request.options.response_cache_store.set(request, response) unless response.cached?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
response
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# will either assign a still-fresh cached response to +request+, or set up its HTTP
|
|
128
|
+
# cache invalidation headers in case it's not fresh anymore.
|
|
129
|
+
def prepare_cache(request)
|
|
130
|
+
cached_response = request.options.response_cache_store.get(request)
|
|
131
|
+
|
|
132
|
+
return unless cached_response && match_by_vary?(request, cached_response)
|
|
133
|
+
|
|
134
|
+
cached_response.body.rewind
|
|
135
|
+
|
|
136
|
+
if cached_response.fresh?
|
|
137
|
+
cached_response = cached_response.dup
|
|
138
|
+
cached_response.mark_as_cached!
|
|
139
|
+
request.response = cached_response
|
|
140
|
+
request.emit(:response, cached_response)
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
request.cached_response = cached_response
|
|
145
|
+
|
|
146
|
+
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
|
147
|
+
request.headers.add("if-modified-since", last_modified)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
|
|
151
|
+
request.headers.add("if-none-match", etag)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cacheable_request?(request)
|
|
156
|
+
request.cacheable_verb? &&
|
|
157
|
+
(
|
|
158
|
+
!request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# whether the +response+ complies with the directives set by the +request+ "vary" header
|
|
163
|
+
# (true when none is available).
|
|
164
|
+
def match_by_vary?(request, response)
|
|
165
|
+
vary = response.vary
|
|
166
|
+
|
|
167
|
+
return true unless vary
|
|
168
|
+
|
|
169
|
+
original_request = response.original_request
|
|
170
|
+
|
|
171
|
+
if vary == %w[*]
|
|
172
|
+
request.options.supported_vary_headers.each do |field|
|
|
173
|
+
return false unless request.headers[field] == original_request.headers[field]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
return true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
vary.all? do |field|
|
|
180
|
+
!original_request.headers.key?(field) || request.headers[field] == original_request.headers[field]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
module RequestMethods
|
|
186
|
+
# points to a previously cached Response corresponding to this request.
|
|
187
|
+
attr_accessor :cached_response
|
|
188
|
+
|
|
189
|
+
def initialize(*)
|
|
190
|
+
super
|
|
191
|
+
@cached_response = nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def merge_headers(*)
|
|
195
|
+
super
|
|
196
|
+
@response_cache_key = nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# returns whether this request is cacheable as per HTTP caching rules.
|
|
200
|
+
def cacheable_verb?
|
|
201
|
+
CACHEABLE_VERBS.include?(@verb)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# returns a unique cache key as a String identifying this request
|
|
205
|
+
def response_cache_key
|
|
206
|
+
@response_cache_key ||= begin
|
|
207
|
+
keys = [@verb, @uri]
|
|
208
|
+
|
|
209
|
+
@options.supported_vary_headers.each do |field|
|
|
210
|
+
value = @headers[field]
|
|
211
|
+
|
|
212
|
+
keys << value if value
|
|
213
|
+
end
|
|
214
|
+
Digest::SHA1.hexdigest("httpx-response-cache-#{keys.join("-")}")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
module ResponseMethods
|
|
220
|
+
attr_writer :original_request
|
|
221
|
+
|
|
222
|
+
def initialize(*)
|
|
223
|
+
super
|
|
224
|
+
@cached = false
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# a copy of the request this response was originally cached from
|
|
228
|
+
def original_request
|
|
229
|
+
@original_request || @request
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# whether this Response was duplicated from a previously {RequestMethods#cached_response}.
|
|
233
|
+
def cached?
|
|
234
|
+
@cached
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# sets this Response as being duplicated from a previously cached response.
|
|
238
|
+
def mark_as_cached!
|
|
239
|
+
@cached = true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# eager-copies the response headers and body from {RequestMethods#cached_response}.
|
|
243
|
+
def copy_from_cached!
|
|
244
|
+
cached_response = @request.cached_response
|
|
245
|
+
|
|
246
|
+
return unless cached_response
|
|
247
|
+
|
|
248
|
+
# 304 responses do not have content-type, which are needed for decoding.
|
|
249
|
+
@headers = @headers.class.new(cached_response.headers.merge(@headers))
|
|
250
|
+
|
|
251
|
+
@body = cached_response.body.dup
|
|
252
|
+
|
|
253
|
+
@body.rewind
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# A response is fresh if its age has not yet exceeded its freshness lifetime.
|
|
257
|
+
# other (#cache_control} directives may influence the outcome, as per the rules
|
|
258
|
+
# from the {rfc}[https://www.rfc-editor.org/rfc/rfc7234]
|
|
259
|
+
def fresh?
|
|
260
|
+
if cache_control
|
|
261
|
+
return false if cache_control.include?("no-cache")
|
|
262
|
+
|
|
263
|
+
return true if cache_control.include?("immutable")
|
|
264
|
+
|
|
265
|
+
# check age: max-age
|
|
266
|
+
max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
|
|
267
|
+
|
|
268
|
+
max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
|
|
269
|
+
|
|
270
|
+
max_age = max_age[/age=(\d+)/, 1] if max_age
|
|
271
|
+
|
|
272
|
+
max_age = max_age.to_i if max_age
|
|
273
|
+
|
|
274
|
+
return max_age > age if max_age
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# check age: expires
|
|
278
|
+
if @headers.key?("expires")
|
|
279
|
+
begin
|
|
280
|
+
expires = Time.httpdate(@headers["expires"])
|
|
281
|
+
rescue ArgumentError
|
|
282
|
+
return false
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
return (expires - Time.now).to_i.positive?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
false
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# returns the "cache-control" directives as an Array of String(s).
|
|
292
|
+
def cache_control
|
|
293
|
+
return @cache_control if defined?(@cache_control)
|
|
294
|
+
|
|
295
|
+
@cache_control = begin
|
|
296
|
+
return unless @headers.key?("cache-control")
|
|
297
|
+
|
|
298
|
+
@headers["cache-control"].split(/ *, */)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# returns the "vary" header value as an Array of (String) headers.
|
|
303
|
+
def vary
|
|
304
|
+
return @vary if defined?(@vary)
|
|
305
|
+
|
|
306
|
+
@vary = begin
|
|
307
|
+
return unless @headers.key?("vary")
|
|
308
|
+
|
|
309
|
+
@headers["vary"].split(/ *, */).map(&:downcase)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
# returns the value of the "age" header as an Integer (time since epoch).
|
|
316
|
+
# if no "age" of header exists, it returns the number of seconds since {#date}.
|
|
317
|
+
def age
|
|
318
|
+
return @headers["age"].to_i if @headers.key?("age")
|
|
319
|
+
|
|
320
|
+
(Time.now - date).to_i
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# returns the value of the "date" header as a Time object
|
|
324
|
+
def date
|
|
325
|
+
@date ||= Time.httpdate(@headers["date"])
|
|
326
|
+
rescue NoMethodError, ArgumentError
|
|
327
|
+
Time.now
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
register_plugin :response_cache, ResponseCache
|
|
332
|
+
end
|
|
333
|
+
end
|