httpx 1.3.4 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/release_notes/1_4_0.md +43 -0
- data/lib/httpx/adapters/faraday.rb +2 -0
- data/lib/httpx/adapters/webmock.rb +11 -5
- data/lib/httpx/callbacks.rb +0 -5
- data/lib/httpx/chainable.rb +3 -1
- data/lib/httpx/connection/http2.rb +11 -7
- data/lib/httpx/connection.rb +128 -16
- data/lib/httpx/errors.rb +12 -0
- data/lib/httpx/loggable.rb +5 -5
- data/lib/httpx/options.rb +26 -16
- data/lib/httpx/plugins/aws_sigv4.rb +31 -16
- data/lib/httpx/plugins/callbacks.rb +12 -2
- data/lib/httpx/plugins/circuit_breaker.rb +0 -5
- data/lib/httpx/plugins/content_digest.rb +202 -0
- data/lib/httpx/plugins/expect.rb +4 -3
- data/lib/httpx/plugins/follow_redirects.rb +7 -8
- data/lib/httpx/plugins/h2c.rb +23 -20
- data/lib/httpx/plugins/internal_telemetry.rb +27 -0
- data/lib/httpx/plugins/persistent.rb +16 -0
- data/lib/httpx/plugins/proxy/http.rb +17 -19
- data/lib/httpx/plugins/proxy.rb +91 -93
- data/lib/httpx/plugins/retries.rb +5 -8
- data/lib/httpx/plugins/upgrade.rb +5 -10
- data/lib/httpx/plugins/webdav.rb +6 -0
- data/lib/httpx/plugins/xml.rb +76 -0
- data/lib/httpx/pool.rb +73 -244
- data/lib/httpx/request/body.rb +16 -12
- data/lib/httpx/request.rb +1 -1
- data/lib/httpx/resolver/https.rb +12 -19
- data/lib/httpx/resolver/multi.rb +34 -16
- data/lib/httpx/resolver/native.rb +36 -13
- data/lib/httpx/resolver/resolver.rb +49 -11
- data/lib/httpx/resolver/system.rb +29 -11
- data/lib/httpx/resolver.rb +21 -14
- data/lib/httpx/response.rb +5 -3
- data/lib/httpx/selector.rb +164 -95
- data/lib/httpx/session.rb +296 -139
- data/lib/httpx/transcoder/gzip.rb +0 -3
- data/lib/httpx/transcoder/json.rb +14 -2
- data/lib/httpx/transcoder/utils/deflater.rb +7 -4
- data/lib/httpx/transcoder/utils/inflater.rb +2 -0
- data/lib/httpx/transcoder.rb +0 -1
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +19 -20
- data/sig/callbacks.rbs +0 -1
- data/sig/chainable.rbs +4 -0
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +14 -3
- data/sig/errors.rbs +6 -0
- data/sig/loggable.rbs +2 -0
- data/sig/options.rbs +7 -0
- data/sig/plugins/aws_sigv4.rbs +8 -2
- data/sig/plugins/content_digest.rbs +51 -0
- data/sig/plugins/cookies/cookie.rbs +9 -0
- data/sig/plugins/grpc/call.rbs +4 -0
- data/sig/plugins/persistent.rbs +4 -1
- data/sig/plugins/proxy/socks5.rbs +11 -3
- data/sig/plugins/proxy.rbs +18 -11
- data/sig/plugins/push_promise.rbs +3 -0
- data/sig/plugins/rate_limiter.rbs +2 -0
- data/sig/plugins/retries.rbs +1 -1
- data/sig/plugins/ssrf_filter.rbs +26 -0
- data/sig/plugins/webdav.rbs +23 -0
- data/sig/plugins/xml.rbs +37 -0
- data/sig/pool.rbs +25 -33
- data/sig/request/body.rbs +5 -1
- data/sig/resolver/multi.rbs +26 -1
- data/sig/resolver/native.rbs +0 -2
- data/sig/resolver/resolver.rbs +21 -2
- data/sig/resolver.rbs +5 -1
- data/sig/response/buffer.rbs +1 -1
- data/sig/selector.rbs +30 -4
- data/sig/session.rbs +45 -18
- data/sig/transcoder/body.rbs +1 -1
- data/sig/transcoder/chunker.rbs +1 -1
- data/sig/transcoder/deflate.rbs +1 -0
- data/sig/transcoder/form.rbs +8 -0
- data/sig/transcoder/gzip.rbs +4 -1
- data/sig/transcoder/utils/body_reader.rbs +2 -2
- data/sig/transcoder/utils/deflater.rbs +2 -2
- metadata +10 -4
- data/lib/httpx/transcoder/xml.rb +0 -52
- data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -31,31 +31,53 @@ module HTTPX
|
|
31
31
|
end
|
32
32
|
|
33
33
|
class Parameters
|
34
|
-
attr_reader :uri, :username, :password, :scheme
|
34
|
+
attr_reader :uri, :username, :password, :scheme, :no_proxy
|
35
35
|
|
36
|
-
def initialize(uri
|
37
|
-
@
|
38
|
-
@
|
39
|
-
|
36
|
+
def initialize(uri: nil, scheme: nil, username: nil, password: nil, no_proxy: nil, **extra)
|
37
|
+
@no_proxy = Array(no_proxy) if no_proxy
|
38
|
+
@uris = Array(uri)
|
39
|
+
uri = @uris.first
|
40
40
|
|
41
|
-
|
41
|
+
@username = username
|
42
|
+
@password = password
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
return
|
44
|
+
@ns = 0
|
45
|
+
|
46
|
+
if uri
|
47
|
+
@uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
|
48
|
+
@username ||= @uri.user
|
49
|
+
@password ||= @uri.password
|
50
50
|
end
|
51
51
|
|
52
52
|
@scheme = scheme
|
53
53
|
|
54
|
-
|
54
|
+
return unless @uri && @username && @password
|
55
55
|
|
56
|
-
|
56
|
+
@authenticator = nil
|
57
|
+
@scheme ||= infer_default_auth_scheme(@uri)
|
58
|
+
|
59
|
+
return unless @scheme
|
57
60
|
|
58
|
-
@authenticator =
|
61
|
+
@authenticator = load_authenticator(@scheme, @username, @password, **extra)
|
62
|
+
end
|
63
|
+
|
64
|
+
def shift
|
65
|
+
# TODO: this operation must be synchronized
|
66
|
+
@ns += 1
|
67
|
+
@uri = @uris[@ns]
|
68
|
+
|
69
|
+
return unless @uri
|
70
|
+
|
71
|
+
@uri = URI(@uri) unless @uri.is_a?(URI::Generic)
|
72
|
+
|
73
|
+
scheme = infer_default_auth_scheme(@uri)
|
74
|
+
|
75
|
+
return unless scheme != @scheme
|
76
|
+
|
77
|
+
@scheme = scheme
|
78
|
+
@username = username || @uri.user
|
79
|
+
@password = password || @uri.password
|
80
|
+
@authenticator = load_authenticator(scheme, @username, @password)
|
59
81
|
end
|
60
82
|
|
61
83
|
def can_authenticate?(*args)
|
@@ -87,6 +109,25 @@ module HTTPX
|
|
87
109
|
super
|
88
110
|
end
|
89
111
|
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def infer_default_auth_scheme(uri)
|
116
|
+
case uri.scheme
|
117
|
+
when "socks5"
|
118
|
+
uri.scheme
|
119
|
+
when "http", "https"
|
120
|
+
"basic"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def load_authenticator(scheme, username, password, **extra)
|
125
|
+
auth_scheme = scheme.to_s.capitalize
|
126
|
+
|
127
|
+
require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
|
128
|
+
|
129
|
+
Authentication.const_get(auth_scheme).new(username, password, **extra)
|
130
|
+
end
|
90
131
|
end
|
91
132
|
|
92
133
|
# adds support for the following options:
|
@@ -95,7 +136,7 @@ module HTTPX
|
|
95
136
|
# *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
|
96
137
|
module OptionsMethods
|
97
138
|
def option_proxy(value)
|
98
|
-
value.is_a?(Parameters) ? value : Hash[value]
|
139
|
+
value.is_a?(Parameters) ? value : Parameters.new(**Hash[value])
|
99
140
|
end
|
100
141
|
|
101
142
|
def option_supported_proxy_protocols(value)
|
@@ -106,97 +147,68 @@ module HTTPX
|
|
106
147
|
end
|
107
148
|
|
108
149
|
module InstanceMethods
|
109
|
-
|
110
|
-
|
111
|
-
def find_connection(request, connections, options)
|
150
|
+
def find_connection(request_uri, selector, options)
|
112
151
|
return super unless options.respond_to?(:proxy)
|
113
152
|
|
114
|
-
|
115
|
-
|
116
|
-
proxy_options = proxy_options(uri, options)
|
117
|
-
|
118
|
-
return super(request, connections, proxy_options) unless proxy_options.proxy
|
119
|
-
|
120
|
-
connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
|
121
|
-
unless connections.nil? || connections.include?(connection)
|
122
|
-
connections << connection
|
123
|
-
set_connection_callbacks(connection, connections, options)
|
153
|
+
if (next_proxy = request_uri.find_proxy)
|
154
|
+
return super(request_uri, selector, options.merge(proxy: Parameters.new(uri: next_proxy)))
|
124
155
|
end
|
125
|
-
connection
|
126
|
-
end
|
127
|
-
|
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
156
|
|
138
|
-
|
157
|
+
proxy = options.proxy
|
139
158
|
|
140
|
-
|
141
|
-
raise Error, "Failed to connect to proxy" unless next_proxy
|
159
|
+
return super unless proxy
|
142
160
|
|
143
|
-
|
161
|
+
next_proxy = proxy.uri
|
144
162
|
|
145
|
-
|
146
|
-
"#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
|
163
|
+
raise Error, "Failed to connect to proxy" unless next_proxy
|
147
164
|
|
148
|
-
|
165
|
+
raise Error,
|
166
|
+
"#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
|
149
167
|
|
150
|
-
|
151
|
-
|
168
|
+
if (no_proxy = proxy.no_proxy)
|
169
|
+
no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
|
152
170
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
proxy.merge(uri: next_proxy)
|
171
|
+
# TODO: setting proxy to nil leaks the connection object in the pool
|
172
|
+
return super(request_uri, selector, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
|
173
|
+
next_proxy.port, no_proxy)
|
158
174
|
end
|
159
175
|
|
160
|
-
|
161
|
-
|
162
|
-
options.merge(proxy: proxy)
|
176
|
+
super(request_uri, selector, options.merge(proxy: proxy))
|
163
177
|
end
|
164
178
|
|
165
|
-
|
166
|
-
response = super
|
179
|
+
private
|
167
180
|
|
168
|
-
|
169
|
-
|
181
|
+
def fetch_response(request, selector, options)
|
182
|
+
response = super
|
170
183
|
|
171
|
-
|
184
|
+
if response.is_a?(ErrorResponse) && proxy_error?(request, response, options)
|
185
|
+
options.proxy.shift
|
172
186
|
|
173
187
|
# return last error response if no more proxies to try
|
174
|
-
return response if
|
188
|
+
return response if options.proxy.uri.nil?
|
175
189
|
|
176
190
|
log { "failed connecting to proxy, trying next..." }
|
177
191
|
request.transition(:idle)
|
178
|
-
send_request(request,
|
192
|
+
send_request(request, selector, options)
|
179
193
|
return
|
180
194
|
end
|
181
195
|
response
|
182
196
|
end
|
183
197
|
|
184
|
-
def proxy_error?(_request, response)
|
198
|
+
def proxy_error?(_request, response, options)
|
199
|
+
return false unless options.proxy
|
200
|
+
|
185
201
|
error = response.error
|
186
202
|
case error
|
187
203
|
when NativeResolveError
|
188
|
-
|
204
|
+
proxy_uri = URI(options.proxy.uri)
|
189
205
|
|
190
|
-
|
191
|
-
|
192
|
-
origin = error.connection.origin
|
206
|
+
peer = error.connection.peer
|
193
207
|
|
194
208
|
# failed resolving proxy domain
|
195
|
-
|
209
|
+
peer.host == proxy_uri.host && peer.port == proxy_uri.port
|
196
210
|
when ResolveError
|
197
|
-
|
198
|
-
|
199
|
-
proxy_uri = URI(@_proxy_uris.first)
|
211
|
+
proxy_uri = URI(options.proxy.uri)
|
200
212
|
|
201
213
|
error.message.end_with?(proxy_uri.to_s)
|
202
214
|
when *PROXY_ERRORS
|
@@ -217,25 +229,11 @@ module HTTPX
|
|
217
229
|
|
218
230
|
# redefining the connection origin as the proxy's URI,
|
219
231
|
# as this will be used as the tcp peer ip.
|
220
|
-
proxy_uri = URI(@options.proxy.uri)
|
221
|
-
@origin.host = proxy_uri.host
|
222
|
-
@origin.port = proxy_uri.port
|
232
|
+
@proxy_uri = URI(@options.proxy.uri)
|
223
233
|
end
|
224
234
|
|
225
|
-
def
|
226
|
-
|
227
|
-
|
228
|
-
if @io.protocol == "h2" &&
|
229
|
-
@origin.scheme == "https" &&
|
230
|
-
connection.origin.scheme == "https" &&
|
231
|
-
@io.can_verify_peer?
|
232
|
-
# in proxied connections, .origin is the proxy ; Given names
|
233
|
-
# are stored in .origins, this is what is used.
|
234
|
-
origin = URI(connection.origins.first)
|
235
|
-
@io.verify_hostname(origin.host)
|
236
|
-
else
|
237
|
-
@origin == connection.origin
|
238
|
-
end
|
235
|
+
def peer
|
236
|
+
@proxy_uri || super
|
239
237
|
end
|
240
238
|
|
241
239
|
def connecting?
|
@@ -261,7 +259,7 @@ module HTTPX
|
|
261
259
|
@state = :open
|
262
260
|
|
263
261
|
super
|
264
|
-
emit(:close)
|
262
|
+
# emit(:close)
|
265
263
|
end
|
266
264
|
|
267
265
|
private
|
@@ -94,7 +94,7 @@ module HTTPX
|
|
94
94
|
|
95
95
|
private
|
96
96
|
|
97
|
-
def fetch_response(request,
|
97
|
+
def fetch_response(request, selector, options)
|
98
98
|
response = super
|
99
99
|
|
100
100
|
if response &&
|
@@ -124,20 +124,17 @@ module HTTPX
|
|
124
124
|
|
125
125
|
retry_start = Utils.now
|
126
126
|
log { "retrying after #{retry_after} secs..." }
|
127
|
-
|
128
|
-
deactivate_connection(request, connections, options)
|
129
|
-
|
130
|
-
pool.after(retry_after) do
|
127
|
+
selector.after(retry_after) do
|
131
128
|
if request.response
|
132
129
|
# request has terminated abruptly meanwhile
|
133
130
|
request.emit(:response, request.response)
|
134
131
|
else
|
135
132
|
log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
|
136
|
-
send_request(request,
|
133
|
+
send_request(request, selector, options)
|
137
134
|
end
|
138
135
|
end
|
139
136
|
else
|
140
|
-
send_request(request,
|
137
|
+
send_request(request, selector, options)
|
141
138
|
end
|
142
139
|
|
143
140
|
return
|
@@ -153,7 +150,7 @@ module HTTPX
|
|
153
150
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
154
151
|
end
|
155
152
|
|
156
|
-
def proxy_error?(request, response)
|
153
|
+
def proxy_error?(request, response, _)
|
157
154
|
super && !request.retries.positive?
|
158
155
|
end
|
159
156
|
|
@@ -28,7 +28,7 @@ module HTTPX
|
|
28
28
|
end
|
29
29
|
|
30
30
|
module InstanceMethods
|
31
|
-
def fetch_response(request,
|
31
|
+
def fetch_response(request, selector, options)
|
32
32
|
response = super
|
33
33
|
|
34
34
|
if response
|
@@ -45,7 +45,7 @@ module HTTPX
|
|
45
45
|
return response unless protocol_handler
|
46
46
|
|
47
47
|
log { "upgrading to #{upgrade_protocol}..." }
|
48
|
-
connection = find_connection(request,
|
48
|
+
connection = find_connection(request.uri, selector, options)
|
49
49
|
|
50
50
|
# do not upgrade already upgraded connections
|
51
51
|
return if connection.upgrade_protocol == upgrade_protocol
|
@@ -60,14 +60,6 @@ module HTTPX
|
|
60
60
|
|
61
61
|
response
|
62
62
|
end
|
63
|
-
|
64
|
-
def close(*args)
|
65
|
-
return super if args.empty?
|
66
|
-
|
67
|
-
connections, = args
|
68
|
-
|
69
|
-
pool.close(connections.reject(&:hijacked))
|
70
|
-
end
|
71
63
|
end
|
72
64
|
|
73
65
|
module ConnectionMethods
|
@@ -75,6 +67,9 @@ module HTTPX
|
|
75
67
|
|
76
68
|
def hijack_io
|
77
69
|
@hijacked = true
|
70
|
+
|
71
|
+
# connection is taken away from selector and not given back to the pool.
|
72
|
+
@current_session.deselect_connection(self, @current_selector, true)
|
78
73
|
end
|
79
74
|
end
|
80
75
|
end
|
data/lib/httpx/plugins/webdav.rb
CHANGED
@@ -8,6 +8,10 @@ module HTTPX
|
|
8
8
|
# https://gitlab.com/os85/httpx/wikis/WebDav
|
9
9
|
#
|
10
10
|
module WebDav
|
11
|
+
def self.configure(klass)
|
12
|
+
klass.plugin(:xml)
|
13
|
+
end
|
14
|
+
|
11
15
|
module InstanceMethods
|
12
16
|
def copy(src, dest)
|
13
17
|
request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
|
@@ -43,6 +47,8 @@ module HTTPX
|
|
43
47
|
ensure
|
44
48
|
unlock(path, lock_token)
|
45
49
|
end
|
50
|
+
|
51
|
+
response
|
46
52
|
end
|
47
53
|
|
48
54
|
def unlock(path, lock_token)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin supports request XML encoding/response decoding using the nokogiri gem.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/XML
|
9
|
+
#
|
10
|
+
module XML
|
11
|
+
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
|
12
|
+
module Transcoder
|
13
|
+
module_function
|
14
|
+
|
15
|
+
class Encoder
|
16
|
+
def initialize(xml)
|
17
|
+
@raw = xml
|
18
|
+
end
|
19
|
+
|
20
|
+
def content_type
|
21
|
+
charset = @raw.respond_to?(:encoding) && @raw.encoding ? @raw.encoding.to_s.downcase : "utf-8"
|
22
|
+
"application/xml; charset=#{charset}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def bytesize
|
26
|
+
@raw.to_s.bytesize
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
@raw.to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def encode(xml)
|
35
|
+
Encoder.new(xml)
|
36
|
+
end
|
37
|
+
|
38
|
+
def decode(response)
|
39
|
+
content_type = response.content_type.mime_type
|
40
|
+
|
41
|
+
raise HTTPX::Error, "invalid form mime type (#{content_type})" unless MIME_TYPES.match?(content_type)
|
42
|
+
|
43
|
+
Nokogiri::XML.method(:parse)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def load_dependencies(*)
|
49
|
+
require "nokogiri"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module ResponseMethods
|
54
|
+
# decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
|
55
|
+
# "application/xml" (requires the "nokogiri" gem).
|
56
|
+
def xml
|
57
|
+
decode(Transcoder)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module RequestBodyClassMethods
|
62
|
+
# ..., xml: Nokogiri::XML::Node #=> xml encoder
|
63
|
+
def initialize_body(params)
|
64
|
+
if (xml = params.delete(:xml))
|
65
|
+
# @type var xml: Nokogiri::XML::Node | String
|
66
|
+
return Transcoder.encode(xml)
|
67
|
+
end
|
68
|
+
|
69
|
+
super
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
register_plugin(:xml, XML)
|
75
|
+
end
|
76
|
+
end
|