httpx 1.7.8 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/doc/release_notes/1_7_1.md +1 -2
- data/doc/release_notes/1_8_0.md +100 -0
- data/doc/release_notes/1_8_1.md +27 -0
- data/lib/httpx/adapters/datadog.rb +12 -1
- data/lib/httpx/adapters/faraday.rb +2 -2
- data/lib/httpx/adapters/webmock.rb +6 -0
- data/lib/httpx/connection/http1.rb +15 -12
- data/lib/httpx/connection/http2.rb +69 -9
- data/lib/httpx/connection.rb +119 -21
- data/lib/httpx/errors.rb +8 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +8 -9
- data/lib/httpx/io/tcp.rb +22 -5
- data/lib/httpx/io/unix.rb +2 -2
- data/lib/httpx/loggable.rb +11 -4
- data/lib/httpx/options.rb +16 -4
- data/lib/httpx/parser/http1.rb +8 -2
- data/lib/httpx/plugins/auth/digest.rb +1 -1
- data/lib/httpx/plugins/auth.rb +61 -4
- data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
- data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
- data/lib/httpx/plugins/cache.rb +221 -0
- data/lib/httpx/plugins/callbacks.rb +12 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +75 -4
- data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
- data/lib/httpx/plugins/oauth.rb +66 -14
- data/lib/httpx/plugins/persistent.rb +18 -2
- data/lib/httpx/plugins/proxy.rb +5 -0
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/plugins/response_cache.rb +26 -105
- data/lib/httpx/plugins/retries.rb +7 -5
- data/lib/httpx/plugins/server_sent_events.rb +158 -0
- data/lib/httpx/plugins/ssrf_filter.rb +17 -2
- data/lib/httpx/plugins/stream.rb +9 -5
- data/lib/httpx/plugins/stream_bidi.rb +2 -0
- data/lib/httpx/plugins/tracing.rb +15 -4
- data/lib/httpx/pool.rb +2 -2
- data/lib/httpx/request/body.rb +2 -2
- data/lib/httpx/request.rb +42 -10
- data/lib/httpx/resolver/cache/base.rb +1 -8
- data/lib/httpx/resolver/cache/file.rb +56 -0
- data/lib/httpx/resolver/https.rb +20 -20
- data/lib/httpx/resolver/native.rb +21 -3
- data/lib/httpx/resolver/resolver.rb +4 -0
- data/lib/httpx/resolver/system.rb +5 -2
- data/lib/httpx/response/body.rb +6 -4
- data/lib/httpx/response.rb +12 -5
- data/lib/httpx/selector.rb +12 -1
- data/lib/httpx/session.rb +6 -1
- data/lib/httpx/session_extensions.rb +2 -2
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +3 -0
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +4 -1
- data/sig/connection.rbs +16 -9
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +2 -0
- data/sig/io/tcp.rbs +2 -0
- data/sig/loggable.rbs +8 -0
- data/sig/options.rbs +25 -12
- data/sig/parser/http1.rbs +3 -1
- data/sig/plugins/auth/ntlm.rbs +1 -1
- data/sig/plugins/auth.rbs +4 -0
- data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
- data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
- data/sig/plugins/cache.rbs +69 -0
- data/sig/plugins/fiber_concurrency.rbs +4 -0
- data/sig/plugins/ntlm_v2_auth.rbs +36 -0
- data/sig/plugins/persistent.rbs +3 -0
- data/sig/plugins/response_cache.rbs +13 -38
- data/sig/plugins/retries.rbs +5 -5
- data/sig/plugins/server_sent_events.rbs +45 -0
- data/sig/plugins/ssrf_filter.rbs +5 -1
- data/sig/plugins/stream.rbs +1 -1
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/webdav.rbs +1 -1
- data/sig/pool.rbs +2 -2
- data/sig/request.rbs +9 -6
- data/sig/resolver/cache/file.rbs +13 -0
- data/sig/resolver/entry.rbs +1 -1
- data/sig/resolver/https.rbs +3 -3
- data/sig/resolver/multi.rbs +1 -1
- data/sig/resolver/native.rbs +5 -5
- data/sig/resolver/resolver.rbs +1 -3
- data/sig/resolver/system.rbs +5 -2
- data/sig/resolver.rbs +3 -0
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +13 -8
- data/sig/timers.rbs +7 -5
- data/sig/transcoder/body.rbs +1 -1
- data/sig/transcoder/gzip.rbs +3 -2
- data/sig/transcoder/multipart.rbs +4 -1
- data/sig/transcoder/utils/deflater.rbs +2 -0
- data/sig/transcoder.rbs +2 -0
- data/sig/utils.rbs +1 -1
- metadata +19 -7
data/lib/httpx/plugins/oauth.rb
CHANGED
|
@@ -12,6 +12,7 @@ module HTTPX
|
|
|
12
12
|
module OAuth
|
|
13
13
|
class << self
|
|
14
14
|
def load_dependencies(klass)
|
|
15
|
+
require "monitor"
|
|
15
16
|
require_relative "auth/basic"
|
|
16
17
|
klass.plugin(:auth)
|
|
17
18
|
end
|
|
@@ -33,8 +34,6 @@ module HTTPX
|
|
|
33
34
|
# Implements the bulk of functionality and maintains the state associated with the
|
|
34
35
|
# management of the the lifecycle of an OAuth session.
|
|
35
36
|
class OAuthSession
|
|
36
|
-
attr_reader :access_token, :refresh_token
|
|
37
|
-
|
|
38
37
|
def initialize(
|
|
39
38
|
issuer:,
|
|
40
39
|
client_id:,
|
|
@@ -62,8 +61,8 @@ module HTTPX
|
|
|
62
61
|
@refresh_token = refresh_token
|
|
63
62
|
@token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
|
|
64
63
|
@grant_type = grant_type || (@refresh_token ? "refresh_token" : "client_credentials")
|
|
65
|
-
@
|
|
66
|
-
@
|
|
64
|
+
@expires_at = nil
|
|
65
|
+
@token_mon = Monitor.new
|
|
67
66
|
|
|
68
67
|
unless @token_endpoint_auth_method.nil? || SUPPORTED_AUTH_METHODS.include?(@token_endpoint_auth_method)
|
|
69
68
|
raise Error, "#{@token_endpoint_auth_method} is not a supported auth method"
|
|
@@ -84,8 +83,24 @@ module HTTPX
|
|
|
84
83
|
@token_endpoint_auth_method || "client_secret_basic"
|
|
85
84
|
end
|
|
86
85
|
|
|
86
|
+
def expires_at
|
|
87
|
+
@token_mon.synchronize { @expires_at }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def access_token
|
|
91
|
+
@token_mon.synchronize do
|
|
92
|
+
if (expires_at = @expires_at) && expires_at < Time.now.to_i
|
|
93
|
+
reset!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@access_token
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
87
100
|
def reset!
|
|
88
|
-
@
|
|
101
|
+
@token_mon.synchronize do
|
|
102
|
+
@access_token = @expires_at = nil
|
|
103
|
+
end
|
|
89
104
|
end
|
|
90
105
|
|
|
91
106
|
# when not available, it uses the +http+ object to request new access and refresh tokens.
|
|
@@ -117,9 +132,11 @@ module HTTPX
|
|
|
117
132
|
when "client_credentials"
|
|
118
133
|
# do nothing
|
|
119
134
|
when "refresh_token"
|
|
120
|
-
|
|
135
|
+
ref_token = refresh_token
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
raise Error, "cannot use the `\"refresh_token\"` grant type without a refresh token" unless ref_token
|
|
138
|
+
|
|
139
|
+
form_post["refresh_token"] = ref_token
|
|
123
140
|
end
|
|
124
141
|
|
|
125
142
|
# POST /token
|
|
@@ -138,8 +155,13 @@ module HTTPX
|
|
|
138
155
|
|
|
139
156
|
payload = token_response.json
|
|
140
157
|
|
|
141
|
-
@
|
|
142
|
-
|
|
158
|
+
@token_mon.synchronize do
|
|
159
|
+
@refresh_token = payload.fetch("refresh_token", @refresh_token)
|
|
160
|
+
if (expires_in = payload["expires_in"])
|
|
161
|
+
@expires_at = Time.now.to_i + Integer(expires_in)
|
|
162
|
+
end
|
|
163
|
+
@access_token = payload["access_token"]
|
|
164
|
+
end
|
|
143
165
|
end
|
|
144
166
|
|
|
145
167
|
# TODO: remove this after deprecating the `:oauth_session` option
|
|
@@ -164,17 +186,23 @@ module HTTPX
|
|
|
164
186
|
|
|
165
187
|
private
|
|
166
188
|
|
|
189
|
+
def refresh_token
|
|
190
|
+
@token_mon.synchronize { @refresh_token }
|
|
191
|
+
end
|
|
192
|
+
|
|
167
193
|
# uses +http+ to fetch for the oauth server metadata.
|
|
168
194
|
def load(http)
|
|
169
195
|
return if @grant_type && @scope
|
|
170
196
|
|
|
171
197
|
metadata = http.skip_auth_header { http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json }
|
|
172
198
|
|
|
173
|
-
@
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
@token_mon.synchronize do
|
|
200
|
+
@token_endpoint = metadata["token_endpoint"]
|
|
201
|
+
@scope = metadata["scopes_supported"]
|
|
202
|
+
@grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
|
|
203
|
+
@token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
|
|
204
|
+
SUPPORTED_AUTH_METHODS.include?(am)
|
|
205
|
+
end
|
|
178
206
|
end
|
|
179
207
|
nil
|
|
180
208
|
end
|
|
@@ -239,6 +267,11 @@ module HTTPX
|
|
|
239
267
|
|
|
240
268
|
@oauth_session.reset!
|
|
241
269
|
@oauth_session.fetch_access_token(self)
|
|
270
|
+
if (expires_at = @oauth_session.expires_at)
|
|
271
|
+
@auth_header_value_mtx.synchronize do
|
|
272
|
+
@auth_header_expires_at = expires_at
|
|
273
|
+
end
|
|
274
|
+
end
|
|
242
275
|
end
|
|
243
276
|
|
|
244
277
|
# TODO: deprecate
|
|
@@ -249,9 +282,18 @@ module HTTPX
|
|
|
249
282
|
other_session = dup # : instance
|
|
250
283
|
oauth_session = other_session.oauth_session
|
|
251
284
|
oauth_session.fetch_access_token(other_session)
|
|
285
|
+
if (expires_at = oauth_session.expires_at)
|
|
286
|
+
@auth_header_expires_at = expires_at
|
|
287
|
+
end
|
|
252
288
|
other_session
|
|
253
289
|
end
|
|
254
290
|
|
|
291
|
+
def reset_auth_header_value!
|
|
292
|
+
super.tap do
|
|
293
|
+
@oauth_session.reset if @oauth_session
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
255
297
|
private
|
|
256
298
|
|
|
257
299
|
def generate_auth_token
|
|
@@ -260,6 +302,16 @@ module HTTPX
|
|
|
260
302
|
@oauth_session.fetch_access_token(self)
|
|
261
303
|
end
|
|
262
304
|
|
|
305
|
+
def set_auth_header_expires_at(_)
|
|
306
|
+
return super unless @oauth_session
|
|
307
|
+
|
|
308
|
+
expires_at = @oauth_session.expires_at
|
|
309
|
+
|
|
310
|
+
return super unless expires_at
|
|
311
|
+
|
|
312
|
+
@auth_header_expires_at = expires_at
|
|
313
|
+
end
|
|
314
|
+
|
|
263
315
|
def dynamic_auth_token?(_)
|
|
264
316
|
@oauth_session
|
|
265
317
|
end
|
|
@@ -55,13 +55,29 @@ module HTTPX
|
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
+
def reconnectable_error?(error)
|
|
59
|
+
Retries::RECONNECTABLE_ERRORS.any? { |klass| error.is_a?(klass) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def when_to_retry(request, response, *)
|
|
63
|
+
return super unless response.is_a?(ErrorResponse)
|
|
64
|
+
|
|
65
|
+
error = response.error
|
|
66
|
+
# allow request to be retried immediately if the request failed right after the keep alive timeout.
|
|
67
|
+
# the chances are, the request failed because the connect has been dropped by the peer server, so it's
|
|
68
|
+
# fine to reopen.
|
|
69
|
+
return if request.ping? && reconnectable_error?(error)
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
58
74
|
def retryable_request?(request, response, *)
|
|
59
75
|
super || begin
|
|
60
|
-
return false unless response
|
|
76
|
+
return false unless response.is_a?(ErrorResponse)
|
|
61
77
|
|
|
62
78
|
error = response.error
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
reconnectable_error?(error)
|
|
65
81
|
end
|
|
66
82
|
end
|
|
67
83
|
|
data/lib/httpx/plugins/proxy.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
3
5
|
module HTTPX
|
|
4
6
|
class ProxyError < ConnectionError; end
|
|
5
7
|
|
|
@@ -65,6 +67,9 @@ module HTTPX
|
|
|
65
67
|
|
|
66
68
|
return unless @scheme
|
|
67
69
|
|
|
70
|
+
@username = CGI.unescape(@username) if @username
|
|
71
|
+
@password = CGI.unescape(@password) if @password
|
|
72
|
+
|
|
68
73
|
@authenticator = load_authenticator(@scheme, @username, @password, **extra)
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -44,9 +44,9 @@ module HTTPX
|
|
|
44
44
|
|
|
45
45
|
def __on_promise_request(parser, stream, h)
|
|
46
46
|
log(level: 1, color: :yellow) do
|
|
47
|
-
# :
|
|
47
|
+
# simplecov:disable
|
|
48
48
|
h.map { |k, v| "#{stream.id}: -> PROMISE HEADER: #{k}: #{v}" }.join("\n")
|
|
49
|
-
# :
|
|
49
|
+
# simplecov:enable
|
|
50
50
|
end
|
|
51
51
|
headers = @options.headers_class.new(h)
|
|
52
52
|
path = headers[":path"]
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module HTTPX
|
|
4
4
|
module Plugins
|
|
5
5
|
#
|
|
6
|
-
# This plugin
|
|
6
|
+
# This plugin caches and reuses responses based on HTTP caching directives defined by
|
|
7
|
+
# the [HTTP Caching RFC](https://www.rfc-editor.org/rfc/rfc9111.html)
|
|
7
8
|
#
|
|
8
9
|
# https://gitlab.com/os85/httpx/wikis/Response-Cache
|
|
9
10
|
#
|
|
@@ -15,9 +16,8 @@ module HTTPX
|
|
|
15
16
|
private_constant :CACHEABLE_STATUS_CODES
|
|
16
17
|
|
|
17
18
|
class << self
|
|
18
|
-
def load_dependencies(
|
|
19
|
-
|
|
20
|
-
require_relative "response_cache/file_store"
|
|
19
|
+
def load_dependencies(klass)
|
|
20
|
+
klass.plugin(:cache)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# whether the +response+ can be stored in the response cache.
|
|
@@ -47,7 +47,6 @@ module HTTPX
|
|
|
47
47
|
def extra_options(options)
|
|
48
48
|
options.merge(
|
|
49
49
|
supported_vary_headers: SUPPORTED_VARY_HEADERS,
|
|
50
|
-
response_cache_store: :store,
|
|
51
50
|
)
|
|
52
51
|
end
|
|
53
52
|
end
|
|
@@ -56,58 +55,18 @@ module HTTPX
|
|
|
56
55
|
#
|
|
57
56
|
# :supported_vary_headers :: array of header values that will be considered for a "vary" header based cache validation
|
|
58
57
|
# (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
58
|
#
|
|
69
59
|
module OptionsMethods
|
|
70
60
|
private
|
|
71
61
|
|
|
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
62
|
def option_supported_vary_headers(value)
|
|
84
63
|
Array(value).sort
|
|
85
64
|
end
|
|
86
65
|
end
|
|
87
66
|
|
|
88
67
|
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
68
|
private
|
|
104
69
|
|
|
105
|
-
def send_request(request, *)
|
|
106
|
-
return request if request.response
|
|
107
|
-
|
|
108
|
-
super
|
|
109
|
-
end
|
|
110
|
-
|
|
111
70
|
def fetch_response(request, *)
|
|
112
71
|
response = super
|
|
113
72
|
|
|
@@ -117,11 +76,7 @@ module HTTPX
|
|
|
117
76
|
log { "returning cached response for #{request.uri}" }
|
|
118
77
|
|
|
119
78
|
response.copy_from_cached!
|
|
120
|
-
|
|
121
|
-
unless response.cached?
|
|
122
|
-
log { "caching response for #{request.uri}..." }
|
|
123
|
-
request.options.response_cache_store.set(request, response)
|
|
124
|
-
end
|
|
79
|
+
|
|
125
80
|
end
|
|
126
81
|
|
|
127
82
|
response
|
|
@@ -130,21 +85,13 @@ module HTTPX
|
|
|
130
85
|
# will either assign a still-fresh cached response to +request+, or set up its HTTP
|
|
131
86
|
# cache invalidation headers in case it's not fresh anymore.
|
|
132
87
|
def prepare_cache(request)
|
|
133
|
-
|
|
88
|
+
super
|
|
134
89
|
|
|
135
|
-
return
|
|
90
|
+
return if request.response # already cached
|
|
136
91
|
|
|
137
|
-
cached_response
|
|
92
|
+
cached_response = retrieve_cached_response(request)
|
|
138
93
|
|
|
139
|
-
|
|
140
|
-
cached_response = cached_response.dup
|
|
141
|
-
cached_response.mark_as_cached!
|
|
142
|
-
request.response = cached_response
|
|
143
|
-
request.emit_response(cached_response)
|
|
144
|
-
return
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
request.cached_response = cached_response
|
|
94
|
+
return unless cached_response && match_by_vary?(request, cached_response)
|
|
148
95
|
|
|
149
96
|
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
|
150
97
|
request.headers.add("if-modified-since", last_modified)
|
|
@@ -156,20 +103,31 @@ module HTTPX
|
|
|
156
103
|
end
|
|
157
104
|
|
|
158
105
|
def cacheable_request?(request)
|
|
159
|
-
|
|
106
|
+
(
|
|
107
|
+
request.cacheable_verb? &&
|
|
160
108
|
(
|
|
161
109
|
!request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
|
|
162
110
|
)
|
|
111
|
+
) || super
|
|
163
112
|
end
|
|
164
113
|
|
|
165
|
-
|
|
114
|
+
def cacheable_response?(_, response)
|
|
115
|
+
ResponseCache.cacheable_response?(response) || super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# +cached_response+ is still valid if it's still fresh
|
|
119
|
+
def valid_cached_response?(_, cached_response)
|
|
120
|
+
cached_response.fresh?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# whether the +cached_response+ complies with the directives set by the +request+ "vary" header
|
|
166
124
|
# (true when none is available).
|
|
167
|
-
def match_by_vary?(request,
|
|
168
|
-
vary =
|
|
125
|
+
def match_by_vary?(request, cached_response)
|
|
126
|
+
vary = cached_response.vary
|
|
169
127
|
|
|
170
128
|
return true unless vary
|
|
171
129
|
|
|
172
|
-
original_request =
|
|
130
|
+
original_request = cached_response.original_request
|
|
173
131
|
|
|
174
132
|
if vary == %w[*]
|
|
175
133
|
request.options.supported_vary_headers.each do |field|
|
|
@@ -186,19 +144,6 @@ module HTTPX
|
|
|
186
144
|
end
|
|
187
145
|
|
|
188
146
|
module RequestMethods
|
|
189
|
-
# points to a previously cached Response corresponding to this request.
|
|
190
|
-
attr_accessor :cached_response
|
|
191
|
-
|
|
192
|
-
def initialize(*)
|
|
193
|
-
super
|
|
194
|
-
@cached_response = nil
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def merge_headers(*)
|
|
198
|
-
super
|
|
199
|
-
@response_cache_key = nil
|
|
200
|
-
end
|
|
201
|
-
|
|
202
147
|
# returns whether this request is cacheable as per HTTP caching rules.
|
|
203
148
|
def cacheable_verb?
|
|
204
149
|
CACHEABLE_VERBS.include?(@verb)
|
|
@@ -220,29 +165,13 @@ module HTTPX
|
|
|
220
165
|
end
|
|
221
166
|
|
|
222
167
|
module ResponseMethods
|
|
223
|
-
attr_writer :
|
|
168
|
+
attr_writer :revalidated_at
|
|
224
169
|
|
|
225
170
|
def initialize(*)
|
|
226
171
|
super
|
|
227
|
-
@cached = false
|
|
228
172
|
@revalidated_at = nil
|
|
229
173
|
end
|
|
230
174
|
|
|
231
|
-
# a copy of the request this response was originally cached from
|
|
232
|
-
def original_request
|
|
233
|
-
@original_request || @request
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# whether this Response was duplicated from a previously {RequestMethods#cached_response}.
|
|
237
|
-
def cached?
|
|
238
|
-
@cached
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# sets this Response as being duplicated from a previously cached response.
|
|
242
|
-
def mark_as_cached!
|
|
243
|
-
@cached = true
|
|
244
|
-
end
|
|
245
|
-
|
|
246
175
|
# eager-copies the response headers and body from {RequestMethods#cached_response}.
|
|
247
176
|
def copy_from_cached!
|
|
248
177
|
cached_response = @request.cached_response
|
|
@@ -333,14 +262,6 @@ module HTTPX
|
|
|
333
262
|
Time.now
|
|
334
263
|
end
|
|
335
264
|
end
|
|
336
|
-
|
|
337
|
-
module ResponseBodyMethods
|
|
338
|
-
def decode_chunk(chunk)
|
|
339
|
-
return chunk if @response.cached?
|
|
340
|
-
|
|
341
|
-
super
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
265
|
end
|
|
345
266
|
register_plugin :response_cache, ResponseCache
|
|
346
267
|
end
|
|
@@ -53,16 +53,18 @@ module HTTPX
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy
|
|
56
|
+
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy,
|
|
57
|
+
# where base is 1 and exponent is 2.
|
|
57
58
|
def retry_after_polynomial_backoff(request, _)
|
|
58
59
|
offset = request.options.max_retries - request.retries
|
|
59
|
-
|
|
60
|
+
1 * ((offset - 1)**2)
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy
|
|
63
|
+
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy,
|
|
64
|
+
# where base is 2
|
|
63
65
|
def retry_after_exponential_backoff(request, _)
|
|
64
66
|
offset = request.options.max_retries - request.retries
|
|
65
|
-
(offset - 1)
|
|
67
|
+
2**(offset - 1)
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -186,7 +188,7 @@ module HTTPX
|
|
|
186
188
|
|
|
187
189
|
# returns whether the +ex+ exception happend for a retriable request.
|
|
188
190
|
def retryable_error?(ex, _)
|
|
189
|
-
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
|
191
|
+
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) } && !ex.is_a?(TotalRequestTimeoutError)
|
|
190
192
|
end
|
|
191
193
|
|
|
192
194
|
def proxy_error?(request, response, _)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin implements convenience methods for Server Sent Events streams.
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Server-Sent-Events
|
|
9
|
+
#
|
|
10
|
+
module ServerSentEvents
|
|
11
|
+
Message = if RUBY_VERSION >= "3.2.0" # rubocop:disable Naming/ConstantName
|
|
12
|
+
Data.define(:data, :event, :id, :retry_after) do
|
|
13
|
+
def initialize(event: nil, id: nil, retry_after: nil, **kwargs)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
Struct.new(:data, :event, :id, :retry_after, keyword_init: true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def subplugins
|
|
23
|
+
{
|
|
24
|
+
retries: ServerSentEventsRetries,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load_dependencies(klass)
|
|
29
|
+
klass.plugin(:stream)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# adds support for the following options:
|
|
34
|
+
#
|
|
35
|
+
# :event_stream :: whether the request is a server-sent events text event stream (defaults to <tt>false</tt>).
|
|
36
|
+
module OptionsMethods
|
|
37
|
+
def option_event_stream(val)
|
|
38
|
+
val
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module InstanceMethods
|
|
43
|
+
def request(*args, **options)
|
|
44
|
+
options[:stream] = true if options[:event_stream]
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_request(*)
|
|
50
|
+
super.tap do |request|
|
|
51
|
+
if request.options.event_stream
|
|
52
|
+
request.headers["accept"] = "text/event-stream"
|
|
53
|
+
request.headers["cache-control"] = "no-cache"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module RequestMethods
|
|
60
|
+
attr_accessor :last_server_sent_message
|
|
61
|
+
|
|
62
|
+
def initialize(*)
|
|
63
|
+
super
|
|
64
|
+
|
|
65
|
+
@last_server_sent_message = nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module StreamResponseMethods
|
|
70
|
+
# yields each event Message as the server emits them.
|
|
71
|
+
def each_message(&block)
|
|
72
|
+
return enum_for(__method__) unless block
|
|
73
|
+
|
|
74
|
+
payload = {}
|
|
75
|
+
|
|
76
|
+
each_line do |line|
|
|
77
|
+
if line.empty?
|
|
78
|
+
if payload[:comment]
|
|
79
|
+
payload.clear
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
next if payload.empty?
|
|
84
|
+
|
|
85
|
+
message = Message.new(**payload)
|
|
86
|
+
|
|
87
|
+
payload.clear
|
|
88
|
+
|
|
89
|
+
@request.last_server_sent_message = message
|
|
90
|
+
|
|
91
|
+
yield message
|
|
92
|
+
else
|
|
93
|
+
type, value = line.split(": ", 2)
|
|
94
|
+
|
|
95
|
+
case type
|
|
96
|
+
when "data"
|
|
97
|
+
type = type.to_sym
|
|
98
|
+
if payload.key?(type)
|
|
99
|
+
payload[type] << "\n" << value
|
|
100
|
+
else
|
|
101
|
+
payload[type] = value
|
|
102
|
+
end
|
|
103
|
+
when "id", "event", "retry"
|
|
104
|
+
type = type.to_sym
|
|
105
|
+
raise_format_error(line) if payload.key?(type) || value.empty?
|
|
106
|
+
|
|
107
|
+
type = :retry_after if type == :retry # avoid using keyword
|
|
108
|
+
|
|
109
|
+
payload[type] = value
|
|
110
|
+
else
|
|
111
|
+
# skip if it's a comment
|
|
112
|
+
if line.start_with?(":")
|
|
113
|
+
payload[:comment] = true
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
raise_format_error(line)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def raise_format_error(line)
|
|
126
|
+
raise Error, "'#{line}': invalid or unsupported event stream format"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
module ServerSentEventsRetries
|
|
131
|
+
module InstanceMethods
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def prepare_to_retry(request, *)
|
|
135
|
+
super
|
|
136
|
+
|
|
137
|
+
last_message = request.last_server_sent_message
|
|
138
|
+
|
|
139
|
+
return unless last_message && last_message.id
|
|
140
|
+
|
|
141
|
+
request.headers["last-event-id"] = last_message.id
|
|
142
|
+
ensure
|
|
143
|
+
request.last_server_sent_message = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def when_to_retry(request, *)
|
|
147
|
+
retry_after = request.last_server_sent_message&.retry_after
|
|
148
|
+
|
|
149
|
+
retry_after / 1_000.0 if retry_after # original in milliseconds
|
|
150
|
+
|
|
151
|
+
request.last_server_sent_message&.retry_after && super
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
register_plugin(:server_sent_events, ServerSentEvents)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -90,12 +90,22 @@ module HTTPX
|
|
|
90
90
|
# adds support for the following options:
|
|
91
91
|
#
|
|
92
92
|
# :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
|
|
93
|
+
# :extra_unsafe_ranges :: A list of IP ranges (or addresses) that will be filtered, in addition to the defaults
|
|
94
|
+
# :safe_private_ranges :: A list of IP ranges (or addresses) that will not be filtered, even if they'd be filtered by default
|
|
93
95
|
module OptionsMethods
|
|
94
96
|
private
|
|
95
97
|
|
|
96
98
|
def option_allowed_schemes(value)
|
|
97
99
|
Array(value)
|
|
98
100
|
end
|
|
101
|
+
|
|
102
|
+
def option_extra_unsafe_ranges(value)
|
|
103
|
+
Array(value).map { |v| v.is_a?(IPAddr) ? v : IPAddr.new(v) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def option_safe_private_ranges(value)
|
|
107
|
+
Array(value).map { |v| v.is_a?(IPAddr) ? v : IPAddr.new(v) }
|
|
108
|
+
end
|
|
99
109
|
end
|
|
100
110
|
|
|
101
111
|
module InstanceMethods
|
|
@@ -132,9 +142,14 @@ module HTTPX
|
|
|
132
142
|
end
|
|
133
143
|
|
|
134
144
|
def addresses=(addrs)
|
|
135
|
-
addrs.reject!
|
|
145
|
+
addrs.reject! do |ipaddr|
|
|
146
|
+
ipaddr = ipaddr.address
|
|
147
|
+
next false if @options.safe_private_ranges&.any? { |r| r.include?(ipaddr) }
|
|
148
|
+
|
|
149
|
+
SsrfFilter.unsafe_ip_address?(ipaddr) || @options.extra_unsafe_ranges&.any? { |r| r.include?(ipaddr) }
|
|
150
|
+
end
|
|
136
151
|
|
|
137
|
-
raise ServerSideRequestForgeryError, "#{@origin.host} has no
|
|
152
|
+
raise ServerSideRequestForgeryError, "#{@origin.host} has no allowed IP addresses" if addrs.empty?
|
|
138
153
|
|
|
139
154
|
super
|
|
140
155
|
end
|