httpx 1.3.4 → 1.4.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_4_0.md +43 -0
- data/doc/release_notes/1_4_1.md +19 -0
- data/lib/httpx/adapters/datadog.rb +55 -83
- data/lib/httpx/adapters/faraday.rb +2 -0
- data/lib/httpx/adapters/webmock.rb +18 -6
- data/lib/httpx/callbacks.rb +0 -5
- data/lib/httpx/chainable.rb +3 -1
- data/lib/httpx/connection/http2.rb +12 -8
- data/lib/httpx/connection.rb +192 -22
- 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/grpc/grpc_encoding.rb +2 -0
- 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 +25 -26
- data/lib/httpx/request.rb +7 -1
- data/lib/httpx/resolver/https.rb +15 -20
- data/lib/httpx/resolver/multi.rb +34 -16
- data/lib/httpx/resolver/native.rb +66 -25
- data/lib/httpx/resolver/resolver.rb +59 -15
- data/lib/httpx/resolver/system.rb +31 -15
- data/lib/httpx/resolver.rb +21 -14
- data/lib/httpx/response.rb +5 -3
- data/lib/httpx/selector.rb +160 -95
- data/lib/httpx/session.rb +273 -140
- data/lib/httpx/transcoder/body.rb +15 -31
- data/lib/httpx/transcoder/gzip.rb +0 -3
- data/lib/httpx/transcoder/json.rb +14 -2
- data/lib/httpx/transcoder/multipart/part.rb +1 -1
- 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 +20 -21
- data/sig/callbacks.rbs +0 -1
- data/sig/chainable.rbs +4 -0
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +29 -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 -9
- data/sig/resolver/multi.rbs +26 -1
- data/sig/resolver/native.rbs +2 -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 +47 -18
- data/sig/transcoder/body.rbs +2 -4
- 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 +3 -3
- data/sig/transcoder/utils/deflater.rbs +3 -3
- metadata +12 -4
- data/lib/httpx/transcoder/xml.rb +0 -52
- data/sig/transcoder/xml.rbs +0 -22
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds `Content-Digest` headers to requests
|
7
|
+
# and can validate these headers on responses
|
8
|
+
#
|
9
|
+
# https://datatracker.ietf.org/doc/html/rfc9530
|
10
|
+
#
|
11
|
+
module ContentDigest
|
12
|
+
class Error < HTTPX::Error; end
|
13
|
+
|
14
|
+
# Error raised on response "content-digest" header validation.
|
15
|
+
class ValidationError < Error
|
16
|
+
attr_reader :response
|
17
|
+
|
18
|
+
def initialize(message, response)
|
19
|
+
super(message)
|
20
|
+
@response = response
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class MissingContentDigestError < ValidationError; end
|
25
|
+
class InvalidContentDigestError < ValidationError; end
|
26
|
+
|
27
|
+
SUPPORTED_ALGORITHMS = {
|
28
|
+
"sha-256" => OpenSSL::Digest::SHA256,
|
29
|
+
"sha-512" => OpenSSL::Digest::SHA512,
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def extra_options(options)
|
34
|
+
options.merge(encode_content_digest: true, validate_content_digest: false, content_digest_algorithm: "sha-256")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# add support for the following options:
|
39
|
+
#
|
40
|
+
# :content_digest_algorithm :: the digest algorithm to use. Currently supports `sha-256` and `sha-512`. (defaults to `sha-256`)
|
41
|
+
# :encode_content_digest :: whether a <tt>Content-Digest</tt> header should be computed for the request;
|
42
|
+
# can also be a callable object (i.e. <tt>->(req) { ... }</tt>, defaults to <tt>true</tt>)
|
43
|
+
# :validate_content_digest :: whether a <tt>Content-Digest</tt> header in the response should be validated;
|
44
|
+
# can also be a callable object (i.e. <tt>->(res) { ... }</tt>, defaults to <tt>false</tt>)
|
45
|
+
module OptionsMethods
|
46
|
+
def option_content_digest_algorithm(value)
|
47
|
+
raise TypeError, ":content_digest_algorithm must be one of 'sha-256', 'sha-512'" unless SUPPORTED_ALGORITHMS.key?(value)
|
48
|
+
|
49
|
+
value
|
50
|
+
end
|
51
|
+
|
52
|
+
def option_encode_content_digest(value)
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
def option_validate_content_digest(value)
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ResponseBodyMethods
|
62
|
+
attr_reader :content_digest_buffer
|
63
|
+
|
64
|
+
def initialize(response, options)
|
65
|
+
super
|
66
|
+
|
67
|
+
return unless response.headers.key?("content-digest")
|
68
|
+
|
69
|
+
should_validate = options.validate_content_digest
|
70
|
+
should_validate = should_validate.call(response) if should_validate.respond_to?(:call)
|
71
|
+
|
72
|
+
return unless should_validate
|
73
|
+
|
74
|
+
@content_digest_buffer = Response::Buffer.new(
|
75
|
+
threshold_size: @options.body_threshold_size,
|
76
|
+
bytesize: @length,
|
77
|
+
encoding: @encoding
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def write(chunk)
|
82
|
+
@content_digest_buffer.write(chunk) if @content_digest_buffer
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def close
|
87
|
+
if @content_digest_buffer
|
88
|
+
@content_digest_buffer.close
|
89
|
+
@content_digest_buffer = nil
|
90
|
+
end
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module InstanceMethods
|
96
|
+
def build_request(*)
|
97
|
+
request = super
|
98
|
+
|
99
|
+
return request if request.empty?
|
100
|
+
|
101
|
+
return request if request.headers.key?("content-digest")
|
102
|
+
|
103
|
+
perform_encoding = @options.encode_content_digest
|
104
|
+
perform_encoding = perform_encoding.call(request) if perform_encoding.respond_to?(:call)
|
105
|
+
|
106
|
+
return request unless perform_encoding
|
107
|
+
|
108
|
+
digest = base64digest(request.body)
|
109
|
+
request.headers.add("content-digest", "#{@options.content_digest_algorithm}=:#{digest}:")
|
110
|
+
|
111
|
+
request
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def fetch_response(request, _, _)
|
117
|
+
response = super
|
118
|
+
return response unless response.is_a?(Response)
|
119
|
+
|
120
|
+
perform_validation = @options.validate_content_digest
|
121
|
+
perform_validation = perform_validation.call(response) if perform_validation.respond_to?(:call)
|
122
|
+
|
123
|
+
validate_content_digest(response) if perform_validation
|
124
|
+
|
125
|
+
response
|
126
|
+
rescue ValidationError => e
|
127
|
+
ErrorResponse.new(request, e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_content_digest(response)
|
131
|
+
content_digest_header = response.headers["content-digest"]
|
132
|
+
|
133
|
+
raise MissingContentDigestError.new("response is missing a `content-digest` header", response) unless content_digest_header
|
134
|
+
|
135
|
+
digests = extract_content_digests(content_digest_header)
|
136
|
+
|
137
|
+
included_algorithms = SUPPORTED_ALGORITHMS.keys & digests.keys
|
138
|
+
|
139
|
+
raise MissingContentDigestError.new("unsupported algorithms: #{digests.keys.join(", ")}", response) if included_algorithms.empty?
|
140
|
+
|
141
|
+
content_buffer = response.body.content_digest_buffer
|
142
|
+
|
143
|
+
included_algorithms.each do |algorithm|
|
144
|
+
digest = SUPPORTED_ALGORITHMS.fetch(algorithm).new
|
145
|
+
digest_received = digests[algorithm]
|
146
|
+
digest_computed =
|
147
|
+
if content_buffer.respond_to?(:to_path)
|
148
|
+
content_buffer.flush
|
149
|
+
digest.file(content_buffer.to_path).base64digest
|
150
|
+
else
|
151
|
+
digest.base64digest(content_buffer.to_s)
|
152
|
+
end
|
153
|
+
|
154
|
+
raise InvalidContentDigestError.new("#{algorithm} digest does not match content",
|
155
|
+
response) unless digest_received == digest_computed
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def extract_content_digests(header)
|
160
|
+
header.split(",").to_h do |entry|
|
161
|
+
algorithm, digest = entry.split("=", 2)
|
162
|
+
raise Error, "#{entry} is an invalid digest format" unless algorithm && digest
|
163
|
+
|
164
|
+
[algorithm, digest.byteslice(1..-2)]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def base64digest(body)
|
169
|
+
digest = SUPPORTED_ALGORITHMS.fetch(@options.content_digest_algorithm).new
|
170
|
+
|
171
|
+
if body.respond_to?(:read)
|
172
|
+
if body.respond_to?(:to_path)
|
173
|
+
digest.file(body.to_path).base64digest
|
174
|
+
else
|
175
|
+
raise ContentDigestError, "request body must be rewindable" unless body.respond_to?(:rewind)
|
176
|
+
|
177
|
+
buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
178
|
+
begin
|
179
|
+
IO.copy_stream(body, buffer)
|
180
|
+
buffer.flush
|
181
|
+
|
182
|
+
digest.file(buffer.to_path).base64digest
|
183
|
+
ensure
|
184
|
+
body.rewind
|
185
|
+
buffer.close
|
186
|
+
buffer.unlink
|
187
|
+
end
|
188
|
+
end
|
189
|
+
else
|
190
|
+
raise ContentDigestError, "base64digest for endless enumerators is not supported" if body.unbounded_body?
|
191
|
+
|
192
|
+
buffer = "".b
|
193
|
+
body.each { |chunk| buffer << chunk }
|
194
|
+
|
195
|
+
digest.base64digest(buffer)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
register_plugin :content_digest, ContentDigest
|
201
|
+
end
|
202
|
+
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -96,15 +96,16 @@ module HTTPX
|
|
96
96
|
end
|
97
97
|
|
98
98
|
module InstanceMethods
|
99
|
-
def fetch_response(request,
|
100
|
-
response =
|
99
|
+
def fetch_response(request, selector, options)
|
100
|
+
response = super
|
101
|
+
|
101
102
|
return unless response
|
102
103
|
|
103
104
|
if response.is_a?(Response) && response.status == 417 && request.headers.key?("expect")
|
104
105
|
response.close
|
105
106
|
request.headers.delete("expect")
|
106
107
|
request.transition(:idle)
|
107
|
-
send_request(request,
|
108
|
+
send_request(request, selector, options)
|
108
109
|
return
|
109
110
|
end
|
110
111
|
|
@@ -64,9 +64,9 @@ module HTTPX
|
|
64
64
|
|
65
65
|
private
|
66
66
|
|
67
|
-
def fetch_response(request,
|
67
|
+
def fetch_response(request, selector, options)
|
68
68
|
redirect_request = request.redirect_request
|
69
|
-
response = super(redirect_request,
|
69
|
+
response = super(redirect_request, selector, options)
|
70
70
|
return unless response
|
71
71
|
|
72
72
|
max_redirects = redirect_request.max_redirects
|
@@ -146,20 +146,19 @@ module HTTPX
|
|
146
146
|
#
|
147
147
|
redirect_after = Utils.parse_retry_after(redirect_after)
|
148
148
|
|
149
|
+
retry_start = Utils.now
|
149
150
|
log { "redirecting after #{redirect_after} secs..." }
|
150
|
-
|
151
|
-
deactivate_connection(request, connections, options)
|
152
|
-
|
153
|
-
pool.after(redirect_after) do
|
151
|
+
selector.after(redirect_after) do
|
154
152
|
if request.response
|
155
153
|
# request has terminated abruptly meanwhile
|
156
154
|
retry_request.emit(:response, request.response)
|
157
155
|
else
|
158
|
-
|
156
|
+
log { "redirecting (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
|
157
|
+
send_request(retry_request, selector, options)
|
159
158
|
end
|
160
159
|
end
|
161
160
|
else
|
162
|
-
send_request(retry_request,
|
161
|
+
send_request(retry_request, selector, options)
|
163
162
|
end
|
164
163
|
nil
|
165
164
|
end
|
data/lib/httpx/plugins/h2c.rb
CHANGED
@@ -25,26 +25,6 @@ module HTTPX
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
module InstanceMethods
|
29
|
-
def send_requests(*requests)
|
30
|
-
upgrade_request, *remainder = requests
|
31
|
-
|
32
|
-
return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
|
33
|
-
|
34
|
-
connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
|
35
|
-
|
36
|
-
return super if connection && connection.upgrade_protocol == "h2c"
|
37
|
-
|
38
|
-
# build upgrade request
|
39
|
-
upgrade_request.headers.add("connection", "upgrade")
|
40
|
-
upgrade_request.headers.add("connection", "http2-settings")
|
41
|
-
upgrade_request.headers["upgrade"] = "h2c"
|
42
|
-
upgrade_request.headers["http2-settings"] = ::HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
|
43
|
-
|
44
|
-
super(upgrade_request, *remainder)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
28
|
class H2CParser < Connection::HTTP2
|
49
29
|
def upgrade(request, response)
|
50
30
|
# skip checks, it is assumed that this is the first
|
@@ -65,6 +45,29 @@ module HTTPX
|
|
65
45
|
module ConnectionMethods
|
66
46
|
using URIExtensions
|
67
47
|
|
48
|
+
def initialize(*)
|
49
|
+
super
|
50
|
+
@h2c_handshake = false
|
51
|
+
end
|
52
|
+
|
53
|
+
def send(request)
|
54
|
+
return super if @h2c_handshake
|
55
|
+
|
56
|
+
return super unless VALID_H2C_VERBS.include?(request.verb) && request.scheme == "http"
|
57
|
+
|
58
|
+
return super if @upgrade_protocol == "h2c"
|
59
|
+
|
60
|
+
@h2c_handshake = true
|
61
|
+
|
62
|
+
# build upgrade request
|
63
|
+
request.headers.add("connection", "upgrade")
|
64
|
+
request.headers.add("connection", "http2-settings")
|
65
|
+
request.headers["upgrade"] = "h2c"
|
66
|
+
request.headers["http2-settings"] = ::HTTP2::Client.settings_header(request.options.http2_settings)
|
67
|
+
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
68
71
|
def upgrade_to_h2c(request, response)
|
69
72
|
prev_parser = @parser
|
70
73
|
|
@@ -76,6 +76,14 @@ module HTTPX
|
|
76
76
|
meter_elapsed_time("Session -> response") if response
|
77
77
|
response
|
78
78
|
end
|
79
|
+
|
80
|
+
def coalesce_connections(conn1, conn2, selector, *)
|
81
|
+
result = super
|
82
|
+
|
83
|
+
meter_elapsed_time("Connection##{conn2.object_id} coalescing to Connection##{conn1.object_id}") if result
|
84
|
+
|
85
|
+
result
|
86
|
+
end
|
79
87
|
end
|
80
88
|
|
81
89
|
module RequestMethods
|
@@ -103,6 +111,25 @@ module HTTPX
|
|
103
111
|
meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
104
112
|
end
|
105
113
|
end
|
114
|
+
|
115
|
+
module PoolMethods
|
116
|
+
def self.included(klass)
|
117
|
+
klass.prepend TrackTimeMethods
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
def checkout_connection(request_uri, options)
|
122
|
+
super.tap do |connection|
|
123
|
+
meter_elapsed_time("Pool##{object_id}: checked out connection for Connection##{connection.object_id}[#{connection.origin}]}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def checkin_connection(connection)
|
128
|
+
super.tap do
|
129
|
+
meter_elapsed_time("Pool##{object_id}: checked in connection for Connection##{connection.object_id}[#{connection.origin}]}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
106
133
|
end
|
107
134
|
register_plugin :internal_telemetry, InternalTelemetry
|
108
135
|
end
|
@@ -30,6 +30,22 @@ module HTTPX
|
|
30
30
|
def self.extra_options(options)
|
31
31
|
options.merge(persistent: true)
|
32
32
|
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
private
|
36
|
+
|
37
|
+
def get_current_selector
|
38
|
+
super(&nil) || begin
|
39
|
+
return unless block_given?
|
40
|
+
|
41
|
+
default = yield
|
42
|
+
|
43
|
+
set_current_selector(default)
|
44
|
+
|
45
|
+
default
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
33
49
|
end
|
34
50
|
register_plugin :persistent, Persistent
|
35
51
|
end
|
@@ -23,29 +23,19 @@ module HTTPX
|
|
23
23
|
with(proxy: opts.merge(scheme: "ntlm"))
|
24
24
|
end
|
25
25
|
|
26
|
-
def fetch_response(request,
|
26
|
+
def fetch_response(request, selector, options)
|
27
27
|
response = super
|
28
28
|
|
29
29
|
if response &&
|
30
30
|
response.is_a?(Response) &&
|
31
31
|
response.status == 407 &&
|
32
32
|
!request.headers.key?("proxy-authorization") &&
|
33
|
-
response.headers.key?("proxy-authenticate")
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
conn.match?(uri, proxy_options)
|
40
|
-
end
|
41
|
-
|
42
|
-
if connection && connection.options.proxy.can_authenticate?(response.headers["proxy-authenticate"])
|
43
|
-
request.transition(:idle)
|
44
|
-
request.headers["proxy-authorization"] =
|
45
|
-
connection.options.proxy.authenticate(request, response.headers["proxy-authenticate"])
|
46
|
-
send_request(request, connections)
|
47
|
-
return
|
48
|
-
end
|
33
|
+
response.headers.key?("proxy-authenticate") && options.proxy.can_authenticate?(response.headers["proxy-authenticate"])
|
34
|
+
request.transition(:idle)
|
35
|
+
request.headers["proxy-authorization"] =
|
36
|
+
options.proxy.authenticate(request, response.headers["proxy-authenticate"])
|
37
|
+
send_request(request, selector, options)
|
38
|
+
return
|
49
39
|
end
|
50
40
|
|
51
41
|
response
|
@@ -74,7 +64,14 @@ module HTTPX
|
|
74
64
|
parser = @parser
|
75
65
|
parser.extend(ProxyParser)
|
76
66
|
parser.on(:response, &method(:__http_on_connect))
|
77
|
-
parser.on(:close)
|
67
|
+
parser.on(:close) do |force|
|
68
|
+
next unless @parser
|
69
|
+
|
70
|
+
if force
|
71
|
+
reset
|
72
|
+
emit(:terminate)
|
73
|
+
end
|
74
|
+
end
|
78
75
|
parser.on(:reset) do
|
79
76
|
if parser.empty?
|
80
77
|
reset
|
@@ -95,8 +92,9 @@ module HTTPX
|
|
95
92
|
|
96
93
|
case @state
|
97
94
|
when :connecting
|
98
|
-
@parser
|
95
|
+
parser = @parser
|
99
96
|
@parser = nil
|
97
|
+
parser.close
|
100
98
|
when :idle
|
101
99
|
@parser.callbacks.clear
|
102
100
|
set_parser_callbacks(@parser)
|