httpx 0.20.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +0 -48
- data/README.md +54 -45
- data/doc/release_notes/0_10_0.md +2 -2
- data/doc/release_notes/0_11_0.md +3 -5
- data/doc/release_notes/0_12_0.md +5 -5
- data/doc/release_notes/0_13_0.md +5 -5
- data/doc/release_notes/0_14_0.md +2 -2
- data/doc/release_notes/0_16_0.md +3 -3
- data/doc/release_notes/0_17_0.md +1 -1
- data/doc/release_notes/0_18_0.md +4 -4
- data/doc/release_notes/0_18_2.md +1 -1
- data/doc/release_notes/0_19_0.md +1 -1
- data/doc/release_notes/0_19_8.md +1 -1
- data/doc/release_notes/0_20_0.md +2 -2
- data/doc/release_notes/0_20_1.md +5 -0
- data/doc/release_notes/0_20_2.md +7 -0
- data/doc/release_notes/0_20_3.md +6 -0
- data/doc/release_notes/0_20_4.md +17 -0
- data/doc/release_notes/0_20_5.md +3 -0
- data/doc/release_notes/0_21_0.md +96 -0
- data/doc/release_notes/0_21_1.md +12 -0
- data/doc/release_notes/0_22_0.md +13 -0
- data/doc/release_notes/0_22_1.md +11 -0
- data/doc/release_notes/0_22_2.md +5 -0
- data/doc/release_notes/0_22_3.md +55 -0
- data/doc/release_notes/0_22_4.md +6 -0
- data/doc/release_notes/0_22_5.md +6 -0
- data/doc/release_notes/0_23_0.md +42 -0
- data/doc/release_notes/0_23_1.md +5 -0
- data/doc/release_notes/0_23_2.md +5 -0
- data/doc/release_notes/0_23_3.md +6 -0
- data/doc/release_notes/0_23_4.md +5 -0
- data/doc/release_notes/0_24_0.md +48 -0
- data/doc/release_notes/0_24_1.md +12 -0
- data/doc/release_notes/0_24_2.md +12 -0
- data/doc/release_notes/0_24_3.md +12 -0
- data/doc/release_notes/0_24_4.md +18 -0
- data/doc/release_notes/0_24_5.md +6 -0
- data/doc/release_notes/0_24_6.md +5 -0
- data/doc/release_notes/0_24_7.md +10 -0
- data/doc/release_notes/1_0_0.md +60 -0
- data/doc/release_notes/1_0_1.md +5 -0
- data/doc/release_notes/1_0_2.md +7 -0
- data/doc/release_notes/1_1_0.md +32 -0
- data/doc/release_notes/1_1_1.md +17 -0
- data/doc/release_notes/1_1_2.md +12 -0
- data/doc/release_notes/1_1_3.md +18 -0
- data/doc/release_notes/1_1_4.md +6 -0
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/doc/release_notes/1_2_1.md +6 -0
- data/doc/release_notes/1_2_2.md +10 -0
- data/doc/release_notes/1_2_3.md +16 -0
- data/doc/release_notes/1_2_4.md +8 -0
- data/doc/release_notes/1_2_5.md +7 -0
- data/doc/release_notes/1_2_6.md +13 -0
- data/doc/release_notes/1_3_0.md +18 -0
- data/doc/release_notes/1_3_1.md +17 -0
- data/lib/httpx/adapters/datadog.rb +215 -122
- data/lib/httpx/adapters/faraday.rb +145 -107
- data/lib/httpx/adapters/sentry.rb +26 -7
- data/lib/httpx/adapters/webmock.rb +34 -18
- data/lib/httpx/altsvc.rb +63 -26
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/buffer.rb +12 -0
- data/lib/httpx/callbacks.rb +5 -3
- data/lib/httpx/chainable.rb +54 -39
- data/lib/httpx/connection/http1.rb +75 -44
- data/lib/httpx/connection/http2.rb +31 -38
- data/lib/httpx/connection.rb +287 -117
- data/lib/httpx/domain_name.rb +10 -13
- data/lib/httpx/errors.rb +52 -2
- data/lib/httpx/extensions.rb +24 -131
- data/lib/httpx/io/ssl.rb +83 -77
- data/lib/httpx/io/tcp.rb +48 -71
- data/lib/httpx/io/udp.rb +18 -52
- data/lib/httpx/io/unix.rb +10 -15
- data/lib/httpx/io.rb +3 -9
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +176 -118
- data/lib/httpx/parser/http1.rb +4 -0
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
- data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
- data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
- data/lib/httpx/plugins/auth.rb +25 -0
- data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
- data/lib/httpx/plugins/aws_sigv4.rb +12 -9
- data/lib/httpx/plugins/basic_auth.rb +29 -0
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/callbacks.rb +91 -0
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
- data/lib/httpx/plugins/circuit_breaker.rb +148 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +30 -17
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
- data/lib/httpx/plugins/expect.rb +21 -14
- data/lib/httpx/plugins/follow_redirects.rb +140 -41
- data/lib/httpx/plugins/grpc/call.rb +2 -3
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
- data/lib/httpx/plugins/grpc/message.rb +7 -37
- data/lib/httpx/plugins/grpc.rb +36 -29
- data/lib/httpx/plugins/h2c.rb +26 -19
- data/lib/httpx/plugins/internal_telemetry.rb +16 -0
- data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
- data/lib/httpx/plugins/oauth.rb +175 -0
- data/lib/httpx/plugins/persistent.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +23 -13
- data/lib/httpx/plugins/proxy/socks4.rb +9 -7
- data/lib/httpx/plugins/proxy/socks5.rb +11 -9
- data/lib/httpx/plugins/proxy.rb +80 -61
- data/lib/httpx/plugins/push_promise.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +5 -1
- data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
- data/lib/httpx/plugins/response_cache/store.rb +62 -25
- data/lib/httpx/plugins/response_cache.rb +105 -12
- data/lib/httpx/plugins/retries.rb +87 -17
- data/lib/httpx/plugins/ssrf_filter.rb +145 -0
- data/lib/httpx/plugins/stream.rb +27 -23
- data/lib/httpx/plugins/upgrade/h2.rb +4 -4
- data/lib/httpx/plugins/upgrade.rb +8 -10
- data/lib/httpx/plugins/webdav.rb +80 -0
- data/lib/httpx/pool/synch_pool.rb +93 -0
- data/lib/httpx/pool.rb +102 -27
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +154 -0
- data/lib/httpx/request.rb +130 -146
- data/lib/httpx/resolver/https.rb +62 -27
- data/lib/httpx/resolver/multi.rb +9 -13
- data/lib/httpx/resolver/native.rb +192 -76
- data/lib/httpx/resolver/resolver.rb +34 -9
- data/lib/httpx/resolver/system.rb +16 -11
- data/lib/httpx/resolver.rb +38 -16
- data/lib/httpx/response/body.rb +242 -0
- data/lib/httpx/response/buffer.rb +96 -0
- data/lib/httpx/response.rb +159 -217
- data/lib/httpx/selector.rb +9 -4
- data/lib/httpx/session.rb +137 -89
- data/lib/httpx/session_extensions.rb +4 -1
- data/lib/httpx/timers.rb +34 -8
- data/lib/httpx/transcoder/body.rb +0 -2
- data/lib/httpx/transcoder/chunker.rb +0 -1
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +52 -33
- data/lib/httpx/transcoder/gzip.rb +74 -0
- data/lib/httpx/transcoder/json.rb +21 -8
- data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
- data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
- data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
- data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
- data/lib/httpx/transcoder/multipart.rb +17 -0
- data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
- data/lib/httpx/transcoder/utils/deflater.rb +72 -0
- data/lib/httpx/transcoder/utils/inflater.rb +19 -0
- data/lib/httpx/transcoder/xml.rb +52 -0
- data/lib/httpx/transcoder.rb +5 -6
- data/lib/httpx/utils.rb +36 -16
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +12 -14
- data/sig/altsvc.rbs +33 -0
- data/sig/buffer.rbs +2 -1
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +11 -9
- data/sig/connection/http1.rbs +8 -7
- data/sig/connection/http2.rbs +19 -19
- data/sig/connection.rbs +64 -24
- data/sig/errors.rbs +22 -3
- data/sig/httpx.rbs +5 -4
- data/sig/io/ssl.rbs +27 -0
- data/sig/io/tcp.rbs +60 -0
- data/sig/io/udp.rbs +20 -0
- data/sig/io/unix.rbs +27 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +32 -22
- data/sig/parser/http1.rbs +1 -1
- data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
- data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
- data/sig/plugins/auth.rbs +13 -0
- data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
- data/sig/plugins/brotli.rbs +22 -0
- data/sig/plugins/callbacks.rbs +38 -0
- data/sig/plugins/circuit_breaker.rbs +71 -0
- data/sig/plugins/compression.rbs +7 -5
- data/sig/plugins/cookies/jar.rbs +2 -2
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
- data/sig/plugins/follow_redirects.rbs +18 -4
- data/sig/plugins/grpc/call.rbs +19 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +7 -32
- data/sig/plugins/h2c.rbs +1 -1
- data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
- data/sig/plugins/oauth.rbs +54 -0
- data/sig/plugins/proxy/http.rbs +3 -0
- data/sig/plugins/proxy/socks4.rbs +9 -6
- data/sig/plugins/proxy/socks5.rbs +10 -6
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/proxy.rbs +13 -5
- data/sig/plugins/push_promise.rbs +3 -3
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache.rbs +36 -7
- data/sig/plugins/retries.rbs +30 -8
- data/sig/plugins/stream.rbs +24 -17
- data/sig/plugins/upgrade.rbs +5 -3
- data/sig/pool.rbs +10 -7
- data/sig/request/body.rbs +38 -0
- data/sig/request.rbs +15 -24
- data/sig/resolver/https.rbs +8 -3
- data/sig/resolver/native.rbs +17 -4
- data/sig/resolver/resolver.rbs +8 -6
- data/sig/resolver/system.rbs +2 -0
- data/sig/resolver.rbs +9 -5
- data/sig/response/body.rbs +53 -0
- data/sig/response/buffer.rbs +24 -0
- data/sig/response.rbs +24 -39
- data/sig/selector.rbs +1 -1
- data/sig/session.rbs +29 -18
- data/sig/timers.rbs +18 -8
- data/sig/transcoder/body.rbs +4 -3
- data/sig/transcoder/deflate.rbs +11 -0
- data/sig/transcoder/form.rbs +5 -3
- data/sig/transcoder/gzip.rbs +24 -0
- data/sig/transcoder/json.rbs +8 -3
- data/sig/{plugins → transcoder}/multipart.rbs +15 -19
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +29 -0
- data/sig/transcoder/utils/inflater.rbs +12 -0
- data/sig/transcoder/xml.rbs +22 -0
- data/sig/transcoder.rbs +24 -9
- data/sig/utils.rbs +8 -2
- metadata +163 -41
- data/lib/httpx/plugins/authentication.rb +0 -20
- data/lib/httpx/plugins/basic_authentication.rb +0 -30
- data/lib/httpx/plugins/compression/brotli.rb +0 -54
- data/lib/httpx/plugins/compression/deflate.rb +0 -49
- data/lib/httpx/plugins/compression/gzip.rb +0 -88
- data/lib/httpx/plugins/compression.rb +0 -164
- data/lib/httpx/plugins/multipart/decoder.rb +0 -187
- data/lib/httpx/plugins/multipart.rb +0 -84
- data/lib/httpx/registry.rb +0 -85
- data/sig/plugins/authentication.rbs +0 -11
- data/sig/plugins/compression/brotli.rbs +0 -21
- data/sig/plugins/compression/deflate.rbs +0 -17
- data/sig/plugins/compression/gzip.rbs +0 -29
- data/sig/registry.rbs +0 -12
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTPX
|
4
|
-
class HTTPProxyError <
|
4
|
+
class HTTPProxyError < ConnectionError; end
|
5
5
|
|
6
6
|
module Plugins
|
7
7
|
#
|
@@ -12,12 +12,24 @@ module HTTPX
|
|
12
12
|
# * Socks4/4a proxies
|
13
13
|
# * Socks5 proxies
|
14
14
|
#
|
15
|
-
# https://gitlab.com/
|
15
|
+
# https://gitlab.com/os85/httpx/wikis/Proxy
|
16
16
|
#
|
17
17
|
module Proxy
|
18
18
|
Error = HTTPProxyError
|
19
19
|
PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
|
20
20
|
|
21
|
+
class << self
|
22
|
+
def configure(klass)
|
23
|
+
klass.plugin(:"proxy/http")
|
24
|
+
klass.plugin(:"proxy/socks4")
|
25
|
+
klass.plugin(:"proxy/socks5")
|
26
|
+
end
|
27
|
+
|
28
|
+
def extra_options(options)
|
29
|
+
options.merge(supported_proxy_protocols: [])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
21
33
|
class Parameters
|
22
34
|
attr_reader :uri, :username, :password, :scheme
|
23
35
|
|
@@ -41,7 +53,7 @@ module HTTPX
|
|
41
53
|
|
42
54
|
auth_scheme = scheme.to_s.capitalize
|
43
55
|
|
44
|
-
require_relative "
|
56
|
+
require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
|
45
57
|
|
46
58
|
@authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
|
47
59
|
end
|
@@ -77,48 +89,35 @@ module HTTPX
|
|
77
89
|
end
|
78
90
|
end
|
79
91
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
klass.plugin(:"proxy/socks5")
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
92
|
+
# adds support for the following options:
|
93
|
+
#
|
94
|
+
# :proxy :: proxy options defining *:uri*, *:username*, *:password* or
|
95
|
+
# *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
|
88
96
|
module OptionsMethods
|
89
97
|
def option_proxy(value)
|
90
98
|
value.is_a?(Parameters) ? value : Hash[value]
|
91
99
|
end
|
100
|
+
|
101
|
+
def option_supported_proxy_protocols(value)
|
102
|
+
raise TypeError, ":supported_proxy_protocols must be an Array" unless value.is_a?(Array)
|
103
|
+
|
104
|
+
value.map(&:to_s)
|
105
|
+
end
|
92
106
|
end
|
93
107
|
|
94
108
|
module InstanceMethods
|
95
109
|
private
|
96
110
|
|
97
|
-
def proxy_uris(uri, options)
|
98
|
-
@_proxy_uris ||= begin
|
99
|
-
uris = options.proxy ? Array(options.proxy[:uri]) : []
|
100
|
-
if uris.empty?
|
101
|
-
uri = URI(uri).find_proxy
|
102
|
-
uris << uri if uri
|
103
|
-
end
|
104
|
-
uris
|
105
|
-
end
|
106
|
-
return if @_proxy_uris.empty?
|
107
|
-
|
108
|
-
proxy_opts = { uri: @_proxy_uris.first }
|
109
|
-
proxy_opts = options.proxy.merge(proxy_opts) if options.proxy
|
110
|
-
proxy_opts
|
111
|
-
end
|
112
|
-
|
113
111
|
def find_connection(request, connections, options)
|
114
112
|
return super unless options.respond_to?(:proxy)
|
115
113
|
|
116
|
-
uri =
|
117
|
-
|
118
|
-
|
114
|
+
uri = request.uri
|
115
|
+
|
116
|
+
proxy_options = proxy_options(uri, options)
|
117
|
+
|
118
|
+
return super(request, connections, proxy_options) unless proxy_options.proxy
|
119
119
|
|
120
|
-
|
121
|
-
connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
|
120
|
+
connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
|
122
121
|
unless connections.nil? || connections.include?(connection)
|
123
122
|
connections << connection
|
124
123
|
set_connection_callbacks(connection, connections, options)
|
@@ -126,40 +125,61 @@ module HTTPX
|
|
126
125
|
connection
|
127
126
|
end
|
128
127
|
|
129
|
-
def
|
130
|
-
|
131
|
-
|
128
|
+
def proxy_options(request_uri, options)
|
129
|
+
proxy_opts = if (next_proxy = request_uri.find_proxy)
|
130
|
+
{ uri: next_proxy }
|
131
|
+
else
|
132
|
+
proxy = options.proxy
|
133
|
+
|
134
|
+
return options unless proxy
|
135
|
+
|
136
|
+
return options.merge(proxy: nil) unless proxy.key?(:uri)
|
137
|
+
|
138
|
+
@_proxy_uris ||= Array(proxy[:uri])
|
139
|
+
|
140
|
+
next_proxy = @_proxy_uris.first
|
141
|
+
raise Error, "Failed to connect to proxy" unless next_proxy
|
142
|
+
|
143
|
+
next_proxy = URI(next_proxy)
|
144
|
+
|
145
|
+
raise Error,
|
146
|
+
"#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
|
132
147
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
148
|
+
if proxy.key?(:no_proxy)
|
149
|
+
|
150
|
+
no_proxy = proxy[:no_proxy]
|
151
|
+
no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
|
152
|
+
|
153
|
+
return options.merge(proxy: nil) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
|
154
|
+
next_proxy.port, no_proxy)
|
155
|
+
end
|
156
|
+
|
157
|
+
proxy.merge(uri: next_proxy)
|
137
158
|
end
|
159
|
+
|
160
|
+
proxy = Parameters.new(**proxy_opts)
|
161
|
+
|
162
|
+
options.merge(proxy: proxy)
|
138
163
|
end
|
139
164
|
|
140
165
|
def fetch_response(request, connections, options)
|
141
166
|
response = super
|
142
167
|
|
143
|
-
if response.is_a?(ErrorResponse) &&
|
144
|
-
__proxy_error?(response) && !@_proxy_uris.empty?
|
168
|
+
if response.is_a?(ErrorResponse) && proxy_error?(request, response)
|
145
169
|
@_proxy_uris.shift
|
170
|
+
|
171
|
+
# return last error response if no more proxies to try
|
172
|
+
return response if @_proxy_uris.empty?
|
173
|
+
|
146
174
|
log { "failed connecting to proxy, trying next..." }
|
147
175
|
request.transition(:idle)
|
148
|
-
|
149
|
-
connections << connection unless connections.include?(connection)
|
150
|
-
connection.send(request)
|
176
|
+
send_request(request, connections, options)
|
151
177
|
return
|
152
178
|
end
|
153
179
|
response
|
154
180
|
end
|
155
181
|
|
156
|
-
def
|
157
|
-
return if options.proxy
|
158
|
-
|
159
|
-
super
|
160
|
-
end
|
161
|
-
|
162
|
-
def __proxy_error?(response)
|
182
|
+
def proxy_error?(_request, response)
|
163
183
|
error = response.error
|
164
184
|
case error
|
165
185
|
when NativeResolveError
|
@@ -216,13 +236,6 @@ module HTTPX
|
|
216
236
|
end
|
217
237
|
end
|
218
238
|
|
219
|
-
def send(request)
|
220
|
-
return super unless @options.proxy
|
221
|
-
return super unless connecting?
|
222
|
-
|
223
|
-
@pending << request
|
224
|
-
end
|
225
|
-
|
226
239
|
def connecting?
|
227
240
|
return super unless @options.proxy
|
228
241
|
|
@@ -244,13 +257,19 @@ module HTTPX
|
|
244
257
|
return super unless @options.proxy
|
245
258
|
|
246
259
|
@state = :open
|
247
|
-
|
248
|
-
|
260
|
+
|
261
|
+
super
|
249
262
|
emit(:close)
|
250
263
|
end
|
251
264
|
|
252
265
|
private
|
253
266
|
|
267
|
+
def initialize_type(uri, options)
|
268
|
+
return super unless options.proxy
|
269
|
+
|
270
|
+
"tcp"
|
271
|
+
end
|
272
|
+
|
254
273
|
def connect
|
255
274
|
return super unless @options.proxy
|
256
275
|
|
@@ -278,7 +297,7 @@ module HTTPX
|
|
278
297
|
register_plugin :proxy, Proxy
|
279
298
|
end
|
280
299
|
|
281
|
-
class ProxySSL <
|
300
|
+
class ProxySSL < SSL
|
282
301
|
def initialize(tcp, request_uri, options)
|
283
302
|
@io = tcp.to_io
|
284
303
|
super(request_uri, tcp.addresses, options)
|
@@ -8,7 +8,7 @@ module HTTPX
|
|
8
8
|
# In order to benefit from this, requests are sent one at a time, so that
|
9
9
|
# no push responses are received after corresponding request has been sent.
|
10
10
|
#
|
11
|
-
# https://gitlab.com/
|
11
|
+
# https://gitlab.com/os85/httpx/wikis/Server-Push
|
12
12
|
#
|
13
13
|
module PushPromise
|
14
14
|
def self.extra_options(options)
|
@@ -9,7 +9,7 @@ module HTTPX
|
|
9
9
|
# * when the server is unavailable (503);
|
10
10
|
# * when a 3xx request comes with a "retry-after" value
|
11
11
|
#
|
12
|
-
# https://gitlab.com/
|
12
|
+
# https://gitlab.com/os85/httpx/wikis/Rate-Limiter
|
13
13
|
#
|
14
14
|
module RateLimiter
|
15
15
|
class << self
|
@@ -23,6 +23,8 @@ module HTTPX
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def retry_on_rate_limited_response(response)
|
26
|
+
return false unless response.is_a?(Response)
|
27
|
+
|
26
28
|
status = response.status
|
27
29
|
|
28
30
|
RATE_LIMIT_CODES.include?(status)
|
@@ -37,6 +39,8 @@ module HTTPX
|
|
37
39
|
# the redirected request.
|
38
40
|
#
|
39
41
|
def retry_after_rate_limit(_, response)
|
42
|
+
return unless response.is_a?(Response)
|
43
|
+
|
40
44
|
retry_after = response.headers["retry-after"]
|
41
45
|
|
42
46
|
return unless retry_after
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require_relative "store"
|
5
|
+
|
6
|
+
module HTTPX::Plugins
|
7
|
+
module ResponseCache
|
8
|
+
class FileStore < Store
|
9
|
+
def initialize(dir = Dir.tmpdir)
|
10
|
+
@dir = Pathname.new(dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
# delete all files
|
15
|
+
end
|
16
|
+
|
17
|
+
def cached?(request)
|
18
|
+
file_path = @dir.join(request.response_cache_key)
|
19
|
+
|
20
|
+
exist?(file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def _get(request)
|
26
|
+
return unless cached?(request)
|
27
|
+
|
28
|
+
File.open(@dir.join(request.response_cache_key))
|
29
|
+
end
|
30
|
+
|
31
|
+
def _set(request, response)
|
32
|
+
file_path = @dir.join(request.response_cache_key)
|
33
|
+
|
34
|
+
response.copy_to(file_path)
|
35
|
+
|
36
|
+
response.body.rewind
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,47 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "forwardable"
|
4
|
-
|
5
3
|
module HTTPX::Plugins
|
6
4
|
module ResponseCache
|
7
5
|
class Store
|
8
|
-
extend Forwardable
|
9
|
-
|
10
|
-
def_delegator :@store, :clear
|
11
|
-
|
12
6
|
def initialize
|
13
7
|
@store = {}
|
8
|
+
@store_mutex = Thread::Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear
|
12
|
+
@store_mutex.synchronize { @store.clear }
|
14
13
|
end
|
15
14
|
|
16
|
-
def lookup(
|
17
|
-
|
15
|
+
def lookup(request)
|
16
|
+
responses = _get(request)
|
17
|
+
|
18
|
+
return unless responses
|
19
|
+
|
20
|
+
responses.find(&method(:match_by_vary?).curry(2)[request])
|
18
21
|
end
|
19
22
|
|
20
|
-
def cached?(
|
21
|
-
|
23
|
+
def cached?(request)
|
24
|
+
lookup(request)
|
22
25
|
end
|
23
26
|
|
24
|
-
def cache(
|
25
|
-
|
27
|
+
def cache(request, response)
|
28
|
+
return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
|
29
|
+
|
30
|
+
_set(request, response)
|
26
31
|
end
|
27
32
|
|
28
33
|
def prepare(request)
|
29
|
-
cached_response =
|
34
|
+
cached_response = lookup(request)
|
30
35
|
|
31
36
|
return unless cached_response
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
if (vary = cached_response.headers["vary"])
|
36
|
-
if vary == "*"
|
37
|
-
return unless request.headers.same_headers?(original_request.headers)
|
38
|
-
else
|
39
|
-
return unless vary.split(/ *, */).all? do |cache_field|
|
40
|
-
cache_field.downcase!
|
41
|
-
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
38
|
+
return unless match_by_vary?(request, cached_response)
|
45
39
|
|
46
40
|
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
47
41
|
request.headers.add("if-modified-since", last_modified)
|
@@ -51,6 +45,49 @@ module HTTPX::Plugins
|
|
51
45
|
request.headers.add("if-none-match", etag)
|
52
46
|
end
|
53
47
|
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def match_by_vary?(request, response)
|
52
|
+
vary = response.vary
|
53
|
+
|
54
|
+
return true unless vary
|
55
|
+
|
56
|
+
original_request = response.instance_variable_get(:@request)
|
57
|
+
|
58
|
+
return request.headers.same_headers?(original_request.headers) if vary == %w[*]
|
59
|
+
|
60
|
+
vary.all? do |cache_field|
|
61
|
+
cache_field.downcase!
|
62
|
+
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def _get(request)
|
67
|
+
@store_mutex.synchronize do
|
68
|
+
responses = @store[request.response_cache_key]
|
69
|
+
|
70
|
+
return unless responses
|
71
|
+
|
72
|
+
responses.select! do |res|
|
73
|
+
!res.body.closed? && res.fresh?
|
74
|
+
end
|
75
|
+
|
76
|
+
responses
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def _set(request, response)
|
81
|
+
@store_mutex.synchronize do
|
82
|
+
responses = (@store[request.response_cache_key] ||= [])
|
83
|
+
|
84
|
+
responses.reject! do |res|
|
85
|
+
res.body.closed? || !res.fresh? || match_by_vary?(request, res)
|
86
|
+
end
|
87
|
+
|
88
|
+
responses << response
|
89
|
+
end
|
90
|
+
end
|
54
91
|
end
|
55
92
|
end
|
56
93
|
end
|
@@ -5,11 +5,13 @@ module HTTPX
|
|
5
5
|
#
|
6
6
|
# This plugin adds support for retrying requests when certain errors happen.
|
7
7
|
#
|
8
|
-
# https://gitlab.com/
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Response-Cache
|
9
9
|
#
|
10
10
|
module ResponseCache
|
11
|
-
CACHEABLE_VERBS = %
|
11
|
+
CACHEABLE_VERBS = %w[GET HEAD].freeze
|
12
|
+
CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
|
12
13
|
private_constant :CACHEABLE_VERBS
|
14
|
+
private_constant :CACHEABLE_STATUS_CODES
|
13
15
|
|
14
16
|
class << self
|
15
17
|
def load_dependencies(*)
|
@@ -17,14 +19,28 @@ module HTTPX
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def cacheable_request?(request)
|
20
|
-
CACHEABLE_VERBS.include?(request.verb)
|
22
|
+
CACHEABLE_VERBS.include?(request.verb) &&
|
23
|
+
(
|
24
|
+
!request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
|
25
|
+
)
|
21
26
|
end
|
22
27
|
|
23
28
|
def cacheable_response?(response)
|
24
29
|
response.is_a?(Response) &&
|
25
|
-
|
30
|
+
(
|
31
|
+
response.cache_control.nil? ||
|
32
|
+
# TODO: !response.cache_control.include?("private") && is shared cache
|
33
|
+
!response.cache_control.include?("no-store")
|
34
|
+
) &&
|
35
|
+
CACHEABLE_STATUS_CODES.include?(response.status) &&
|
36
|
+
# RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
|
37
|
+
# 410 MAY be stored by a cache and used in reply to a subsequent
|
38
|
+
# request, subject to the expiration mechanism, unless a cache-control
|
39
|
+
# directive prohibits caching. However, a cache that does not support
|
40
|
+
# the Range and Content-Range headers MUST NOT cache 206 (Partial
|
41
|
+
# Content) responses.
|
26
42
|
response.status != 206 && (
|
27
|
-
response.headers.key?("etag") || response.headers.key?("last-modified
|
43
|
+
response.headers.key?("etag") || response.headers.key?("last-modified") || response.fresh?
|
28
44
|
)
|
29
45
|
end
|
30
46
|
|
@@ -52,7 +68,7 @@ module HTTPX
|
|
52
68
|
|
53
69
|
def build_request(*)
|
54
70
|
request = super
|
55
|
-
return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request
|
71
|
+
return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request)
|
56
72
|
|
57
73
|
@options.response_cache_store.prepare(request)
|
58
74
|
|
@@ -62,24 +78,101 @@ module HTTPX
|
|
62
78
|
def fetch_response(request, *)
|
63
79
|
response = super
|
64
80
|
|
65
|
-
|
81
|
+
return unless response
|
82
|
+
|
83
|
+
if ResponseCache.cached_response?(response)
|
66
84
|
log { "returning cached response for #{request.uri}" }
|
67
|
-
cached_response = @options.response_cache_store.lookup(request
|
85
|
+
cached_response = @options.response_cache_store.lookup(request)
|
68
86
|
|
69
87
|
response.copy_from_cached(cached_response)
|
70
|
-
end
|
71
88
|
|
72
|
-
|
89
|
+
else
|
90
|
+
@options.response_cache_store.cache(request, response)
|
91
|
+
end
|
73
92
|
|
74
93
|
response
|
75
94
|
end
|
76
95
|
end
|
77
96
|
|
97
|
+
module RequestMethods
|
98
|
+
def response_cache_key
|
99
|
+
@response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}-#{@uri}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
78
103
|
module ResponseMethods
|
79
104
|
def copy_from_cached(other)
|
80
|
-
|
105
|
+
# 304 responses do not have content-type, which are needed for decoding.
|
106
|
+
@headers = @headers.class.new(other.headers.merge(@headers))
|
107
|
+
|
108
|
+
@body = other.body.dup
|
109
|
+
|
110
|
+
@body.rewind
|
111
|
+
end
|
112
|
+
|
113
|
+
# A response is fresh if its age has not yet exceeded its freshness lifetime.
|
114
|
+
def fresh?
|
115
|
+
if cache_control
|
116
|
+
return false if cache_control.include?("no-cache")
|
117
|
+
|
118
|
+
# check age: max-age
|
119
|
+
max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
|
120
|
+
|
121
|
+
max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
|
122
|
+
|
123
|
+
max_age = max_age[/age=(\d+)/, 1] if max_age
|
124
|
+
|
125
|
+
max_age = max_age.to_i if max_age
|
126
|
+
|
127
|
+
return max_age > age if max_age
|
128
|
+
end
|
129
|
+
|
130
|
+
# check age: expires
|
131
|
+
if @headers.key?("expires")
|
132
|
+
begin
|
133
|
+
expires = Time.httpdate(@headers["expires"])
|
134
|
+
rescue ArgumentError
|
135
|
+
return true
|
136
|
+
end
|
137
|
+
|
138
|
+
return (expires - Time.now).to_i.positive?
|
139
|
+
end
|
140
|
+
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def cache_control
|
145
|
+
return @cache_control if defined?(@cache_control)
|
146
|
+
|
147
|
+
@cache_control = begin
|
148
|
+
return unless @headers.key?("cache-control")
|
149
|
+
|
150
|
+
@headers["cache-control"].split(/ *, */)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def vary
|
155
|
+
return @vary if defined?(@vary)
|
156
|
+
|
157
|
+
@vary = begin
|
158
|
+
return unless @headers.key?("vary")
|
159
|
+
|
160
|
+
@headers["vary"].split(/ *, */)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def age
|
167
|
+
return @headers["age"].to_i if @headers.key?("age")
|
168
|
+
|
169
|
+
(Time.now - date).to_i
|
170
|
+
end
|
81
171
|
|
82
|
-
|
172
|
+
def date
|
173
|
+
@date ||= Time.httpdate(@headers["date"])
|
174
|
+
rescue NoMethodError, ArgumentError
|
175
|
+
Time.now
|
83
176
|
end
|
84
177
|
end
|
85
178
|
end
|