httpx 0.15.4 → 0.18.0
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/0_16_0.md +93 -0
- data/doc/release_notes/0_16_1.md +5 -0
- data/doc/release_notes/0_17_0.md +49 -0
- data/doc/release_notes/0_18_0.md +69 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +8 -14
- data/lib/httpx/adapters/webmock.rb +9 -3
- data/lib/httpx/altsvc.rb +2 -2
- data/lib/httpx/buffer.rb +1 -1
- data/lib/httpx/callbacks.rb +1 -1
- data/lib/httpx/chainable.rb +18 -11
- data/lib/httpx/connection/http1.rb +21 -13
- data/lib/httpx/connection/http2.rb +20 -25
- data/lib/httpx/connection.rb +73 -77
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/errors.rb +11 -11
- data/lib/httpx/extensions.rb +50 -4
- data/lib/httpx/headers.rb +1 -1
- data/lib/httpx/io/ssl.rb +3 -3
- data/lib/httpx/io/tls.rb +8 -8
- data/lib/httpx/loggable.rb +5 -5
- data/lib/httpx/options.rb +108 -81
- data/lib/httpx/parser/http1.rb +11 -7
- data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
- data/lib/httpx/plugins/aws_sigv4.rb +19 -20
- data/lib/httpx/plugins/compression.rb +17 -14
- data/lib/httpx/plugins/cookies/cookie.rb +4 -2
- data/lib/httpx/plugins/cookies/jar.rb +21 -2
- data/lib/httpx/plugins/cookies.rb +20 -7
- data/lib/httpx/plugins/digest_authentication.rb +19 -15
- data/lib/httpx/plugins/expect.rb +26 -18
- data/lib/httpx/plugins/follow_redirects.rb +9 -9
- data/lib/httpx/plugins/grpc/call.rb +4 -1
- data/lib/httpx/plugins/grpc/message.rb +2 -2
- data/lib/httpx/plugins/grpc.rb +72 -46
- data/lib/httpx/plugins/h2c.rb +7 -3
- data/lib/httpx/plugins/internal_telemetry.rb +8 -8
- data/lib/httpx/plugins/multipart/decoder.rb +187 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/multipart.rb +16 -2
- data/lib/httpx/plugins/ntlm_authentication.rb +12 -10
- data/lib/httpx/plugins/proxy/socks4.rb +2 -1
- data/lib/httpx/plugins/proxy/socks5.rb +2 -1
- data/lib/httpx/plugins/proxy/ssh.rb +20 -13
- data/lib/httpx/plugins/proxy.rb +10 -10
- data/lib/httpx/plugins/response_cache/store.rb +55 -0
- data/lib/httpx/plugins/response_cache.rb +88 -0
- data/lib/httpx/plugins/retries.rb +46 -23
- data/lib/httpx/plugins/stream.rb +3 -4
- data/lib/httpx/plugins/upgrade.rb +7 -6
- data/lib/httpx/pool.rb +39 -13
- data/lib/httpx/registry.rb +2 -2
- data/lib/httpx/request.rb +16 -25
- data/lib/httpx/resolver/https.rb +4 -8
- data/lib/httpx/resolver/native.rb +19 -5
- data/lib/httpx/resolver/resolver_mixin.rb +2 -1
- data/lib/httpx/resolver/system.rb +2 -0
- data/lib/httpx/resolver.rb +2 -2
- data/lib/httpx/response.rb +91 -48
- data/lib/httpx/selector.rb +11 -24
- data/lib/httpx/session.rb +41 -23
- data/lib/httpx/session2.rb +23 -0
- data/lib/httpx/timers.rb +84 -0
- data/lib/httpx/transcoder/body.rb +3 -2
- data/lib/httpx/transcoder/chunker.rb +2 -1
- data/lib/httpx/transcoder/form.rb +20 -0
- data/lib/httpx/transcoder/json.rb +12 -0
- data/lib/httpx/transcoder.rb +62 -1
- data/lib/httpx/utils.rb +10 -2
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +7 -3
- data/sig/buffer.rbs +3 -1
- data/sig/chainable.rbs +31 -29
- data/sig/connection/http1.rbs +11 -5
- data/sig/connection/http2.rbs +16 -5
- data/sig/connection.rbs +31 -13
- data/sig/errors.rbs +35 -1
- data/sig/headers.rbs +20 -19
- data/sig/httpx.rbs +4 -1
- data/sig/loggable.rbs +3 -1
- data/sig/options.rbs +45 -34
- data/sig/parser/http1.rbs +3 -3
- data/sig/plugins/authentication.rbs +1 -1
- data/sig/plugins/aws_sdk_authentication.rbs +25 -3
- data/sig/plugins/aws_sigv4.rbs +13 -5
- data/sig/plugins/basic_authentication.rbs +1 -1
- data/sig/plugins/compression.rbs +4 -6
- data/sig/plugins/cookies/cookie.rbs +5 -7
- data/sig/plugins/cookies/jar.rbs +9 -10
- data/sig/plugins/cookies.rbs +4 -5
- data/sig/plugins/digest_authentication.rbs +2 -3
- data/sig/plugins/expect.rbs +2 -4
- data/sig/plugins/follow_redirects.rbs +3 -5
- data/sig/plugins/grpc.rbs +4 -7
- data/sig/plugins/h2c.rbs +0 -2
- data/sig/plugins/multipart.rbs +64 -10
- data/sig/plugins/ntlm_authentication.rbs +2 -3
- data/sig/plugins/persistent.rbs +3 -8
- data/sig/plugins/proxy/ssh.rbs +4 -4
- data/sig/plugins/proxy.rbs +13 -13
- data/sig/plugins/push_promise.rbs +0 -2
- data/sig/plugins/response_cache.rbs +35 -0
- data/sig/plugins/retries.rbs +7 -8
- data/sig/plugins/stream.rbs +1 -1
- data/sig/plugins/upgrade.rbs +2 -3
- data/sig/pool.rbs +7 -2
- data/sig/registry.rbs +1 -1
- data/sig/request.rbs +11 -8
- data/sig/resolver/native.rbs +10 -5
- data/sig/resolver/resolver_mixin.rbs +4 -5
- data/sig/resolver/system.rbs +4 -0
- data/sig/resolver.rbs +7 -0
- data/sig/response.rbs +26 -13
- data/sig/selector.rbs +11 -9
- data/sig/session.rbs +22 -23
- data/sig/timers.rbs +32 -0
- data/sig/transcoder/body.rbs +6 -1
- data/sig/transcoder/chunker.rbs +8 -2
- data/sig/transcoder/form.rbs +3 -1
- data/sig/transcoder/json.rbs +2 -0
- data/sig/transcoder.rbs +13 -5
- data/sig/utils.rbs +6 -0
- metadata +18 -18
- data/lib/httpx/request2.rb +0 -14
|
@@ -14,19 +14,23 @@ module HTTPX
|
|
|
14
14
|
|
|
15
15
|
DigestError = Class.new(Error)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
class << self
|
|
18
|
+
def extra_options(options)
|
|
19
|
+
options.merge(max_concurrent_requests: 1)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def load_dependencies(*)
|
|
23
|
+
require "securerandom"
|
|
24
|
+
require "digest"
|
|
25
|
+
end
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
module OptionsMethods
|
|
29
|
+
def option_digest(value)
|
|
30
|
+
raise TypeError, ":digest must be a Digest" unless value.is_a?(Digest)
|
|
31
|
+
|
|
32
|
+
value
|
|
33
|
+
end
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
module InstanceMethods
|
|
@@ -36,12 +40,12 @@ module HTTPX
|
|
|
36
40
|
|
|
37
41
|
alias_method :digest_auth, :digest_authentication
|
|
38
42
|
|
|
39
|
-
def send_requests(*requests
|
|
43
|
+
def send_requests(*requests)
|
|
40
44
|
requests.flat_map do |request|
|
|
41
45
|
digest = request.options.digest
|
|
42
46
|
|
|
43
47
|
if digest
|
|
44
|
-
probe_response = wrap { super(request
|
|
48
|
+
probe_response = wrap { super(request).first }
|
|
45
49
|
|
|
46
50
|
if digest && !probe_response.is_a?(ErrorResponse) &&
|
|
47
51
|
probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
|
|
@@ -52,12 +56,12 @@ module HTTPX
|
|
|
52
56
|
token = digest.generate_header(request, probe_response)
|
|
53
57
|
request.headers["authorization"] = "Digest #{token}"
|
|
54
58
|
|
|
55
|
-
super(request
|
|
59
|
+
super(request)
|
|
56
60
|
else
|
|
57
61
|
probe_response
|
|
58
62
|
end
|
|
59
63
|
else
|
|
60
|
-
super(request
|
|
64
|
+
super(request)
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
|
@@ -10,26 +10,30 @@ module HTTPX
|
|
|
10
10
|
module Expect
|
|
11
11
|
EXPECT_TIMEOUT = 2
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
class << self
|
|
14
|
+
def no_expect_store
|
|
15
|
+
@no_expect_store ||= []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extra_options(options)
|
|
19
|
+
options.merge(expect_timeout: EXPECT_TIMEOUT)
|
|
20
|
+
end
|
|
15
21
|
end
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
raise Error, ":expect_timeout must be positive" unless seconds.positive?
|
|
23
|
+
module OptionsMethods
|
|
24
|
+
def option_expect_timeout(value)
|
|
25
|
+
seconds = Integer(value)
|
|
26
|
+
raise TypeError, ":expect_timeout must be positive" unless seconds.positive?
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
seconds
|
|
29
|
+
end
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
def option_expect_threshold_size(value)
|
|
32
|
+
bytes = Integer(value)
|
|
33
|
+
raise TypeError, ":expect_threshold_size must be positive" unless bytes.positive?
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
|
|
35
|
+
bytes
|
|
36
|
+
end
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
module RequestMethods
|
|
@@ -65,9 +69,14 @@ module HTTPX
|
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
module ConnectionMethods
|
|
68
|
-
def
|
|
72
|
+
def send_request_to_parser(request)
|
|
73
|
+
super
|
|
74
|
+
|
|
75
|
+
return unless request.headers["expect"] == "100-continue"
|
|
76
|
+
|
|
69
77
|
request.once(:expect) do
|
|
70
|
-
@timers.after(
|
|
78
|
+
@timers.after(request.options.expect_timeout) do
|
|
79
|
+
# expect timeout expired
|
|
71
80
|
if request.state == :expect && !request.expects?
|
|
72
81
|
Expect.no_expect_store << request.origin
|
|
73
82
|
request.headers.delete("expect")
|
|
@@ -75,7 +84,6 @@ module HTTPX
|
|
|
75
84
|
end
|
|
76
85
|
end
|
|
77
86
|
end
|
|
78
|
-
super
|
|
79
87
|
end
|
|
80
88
|
end
|
|
81
89
|
|
|
@@ -17,17 +17,17 @@ module HTTPX
|
|
|
17
17
|
MAX_REDIRECTS = 3
|
|
18
18
|
REDIRECT_STATUS = (300..399).freeze
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
raise Error, ":max_redirects must be positive" if num.negative?
|
|
20
|
+
module OptionsMethods
|
|
21
|
+
def option_max_redirects(value)
|
|
22
|
+
num = Integer(value)
|
|
23
|
+
raise TypeError, ":max_redirects must be positive" if num.negative?
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
num
|
|
26
|
+
end
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
def option_follow_insecure_redirects(value)
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
module InstanceMethods
|
|
@@ -10,6 +10,7 @@ module HTTPX
|
|
|
10
10
|
def initialize(response)
|
|
11
11
|
@response = response
|
|
12
12
|
@decoder = ->(z) { z }
|
|
13
|
+
@consumed = false
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def inspect
|
|
@@ -25,7 +26,7 @@ module HTTPX
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def trailing_metadata
|
|
28
|
-
return unless @
|
|
29
|
+
return unless @consumed
|
|
29
30
|
|
|
30
31
|
@response.trailing_metadata
|
|
31
32
|
end
|
|
@@ -40,8 +41,10 @@ module HTTPX
|
|
|
40
41
|
Message.stream(@response).each do |message|
|
|
41
42
|
y << @decoder.call(message)
|
|
42
43
|
end
|
|
44
|
+
@consumed = true
|
|
43
45
|
end
|
|
44
46
|
else
|
|
47
|
+
@consumed = true
|
|
45
48
|
@decoder.call(Message.unary(@response))
|
|
46
49
|
end
|
|
47
50
|
end
|
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
|
17
17
|
|
|
18
18
|
# lazy decodes a grpc stream response
|
|
19
19
|
def stream(response, &block)
|
|
20
|
-
return enum_for(__method__, response) unless
|
|
20
|
+
return enum_for(__method__, response) unless block
|
|
21
21
|
|
|
22
22
|
response.each do |frame|
|
|
23
23
|
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
|
@@ -57,7 +57,7 @@ module HTTPX
|
|
|
57
57
|
|
|
58
58
|
yield data
|
|
59
59
|
|
|
60
|
-
message = message.byteslice(5 + size..-1)
|
|
60
|
+
message = message.byteslice((5 + size)..-1)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
data/lib/httpx/plugins/grpc.rb
CHANGED
|
@@ -49,7 +49,6 @@ module HTTPX
|
|
|
49
49
|
class << self
|
|
50
50
|
def load_dependencies(*)
|
|
51
51
|
require "stringio"
|
|
52
|
-
require "google/protobuf"
|
|
53
52
|
require "httpx/plugins/grpc/message"
|
|
54
53
|
require "httpx/plugins/grpc/call"
|
|
55
54
|
end
|
|
@@ -61,36 +60,7 @@ module HTTPX
|
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def extra_options(options)
|
|
64
|
-
|
|
65
|
-
def_option(:grpc_service, <<-OUT)
|
|
66
|
-
String(value)
|
|
67
|
-
OUT
|
|
68
|
-
|
|
69
|
-
def_option(:grpc_compression, <<-OUT)
|
|
70
|
-
case value
|
|
71
|
-
when true, false
|
|
72
|
-
value
|
|
73
|
-
else
|
|
74
|
-
value.to_s
|
|
75
|
-
end
|
|
76
|
-
OUT
|
|
77
|
-
|
|
78
|
-
def_option(:grpc_rpcs, <<-OUT)
|
|
79
|
-
Hash[value]
|
|
80
|
-
OUT
|
|
81
|
-
|
|
82
|
-
def_option(:grpc_deadline, <<-OUT)
|
|
83
|
-
raise Error, ":grpc_deadline must be positive" unless value.positive?
|
|
84
|
-
|
|
85
|
-
value
|
|
86
|
-
OUT
|
|
87
|
-
|
|
88
|
-
def_option(:call_credentials, <<-OUT)
|
|
89
|
-
raise Error, ":call_credentials must respond to #call" unless value.respond_to?(:call)
|
|
90
|
-
|
|
91
|
-
value
|
|
92
|
-
OUT
|
|
93
|
-
end.new(options).merge(
|
|
63
|
+
options.merge(
|
|
94
64
|
fallback_protocol: "h2",
|
|
95
65
|
http2_settings: { wait_for_handshake: false },
|
|
96
66
|
grpc_rpcs: {}.freeze,
|
|
@@ -100,6 +70,37 @@ module HTTPX
|
|
|
100
70
|
end
|
|
101
71
|
end
|
|
102
72
|
|
|
73
|
+
module OptionsMethods
|
|
74
|
+
def option_grpc_service(value)
|
|
75
|
+
String(value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def option_grpc_compression(value)
|
|
79
|
+
case value
|
|
80
|
+
when true, false
|
|
81
|
+
value
|
|
82
|
+
else
|
|
83
|
+
value.to_s
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def option_grpc_rpcs(value)
|
|
88
|
+
Hash[value]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def option_grpc_deadline(value)
|
|
92
|
+
raise TypeError, ":grpc_deadline must be positive" unless value.positive?
|
|
93
|
+
|
|
94
|
+
value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def option_call_credentials(value)
|
|
98
|
+
raise TypeError, ":call_credentials must respond to #call" unless value.respond_to?(:call)
|
|
99
|
+
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
103
104
|
module ResponseMethods
|
|
104
105
|
attr_reader :trailing_metadata
|
|
105
106
|
|
|
@@ -140,9 +141,19 @@ module HTTPX
|
|
|
140
141
|
deadline: @options.grpc_deadline,
|
|
141
142
|
}.merge(opts)
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
session_class = Class.new(self.class) do
|
|
145
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
146
|
+
def #{rpc_name}(input, **opts) # def grpc_action(input, **opts)
|
|
147
|
+
rpc_execute("#{rpc_name}", input, **opts) # rpc_execute("grpc_action", input, **opts)
|
|
148
|
+
end # end
|
|
149
|
+
OUT
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
session_class.new(@options.merge(
|
|
153
|
+
grpc_rpcs: @options.grpc_rpcs.merge(
|
|
154
|
+
rpc_name.underscore => [rpc_name, input, output, rpc_opts]
|
|
155
|
+
).freeze
|
|
156
|
+
))
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
def build_stub(origin, service: nil, compression: false)
|
|
@@ -150,7 +161,32 @@ module HTTPX
|
|
|
150
161
|
|
|
151
162
|
origin = URI.parse("#{scheme}://#{origin}")
|
|
152
163
|
|
|
153
|
-
|
|
164
|
+
session = self
|
|
165
|
+
|
|
166
|
+
if service && service.respond_to?(:rpc_descs)
|
|
167
|
+
# it's a grpc generic service
|
|
168
|
+
service.rpc_descs.each do |rpc_name, rpc_desc|
|
|
169
|
+
rpc_opts = {
|
|
170
|
+
marshal_method: rpc_desc.marshal_method,
|
|
171
|
+
unmarshal_method: rpc_desc.unmarshal_method,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
input = rpc_desc.input
|
|
175
|
+
input = input.type if input.respond_to?(:type)
|
|
176
|
+
|
|
177
|
+
output = rpc_desc.output
|
|
178
|
+
if output.respond_to?(:type)
|
|
179
|
+
rpc_opts[:stream] = true
|
|
180
|
+
output = output.type
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
session = session.rpc(rpc_name, input, output, **rpc_opts)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
service = service.service_name
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
session.with(origin: origin, grpc_service: service, grpc_compression: compression)
|
|
154
190
|
end
|
|
155
191
|
|
|
156
192
|
def execute(rpc_method, input,
|
|
@@ -166,7 +202,7 @@ module HTTPX
|
|
|
166
202
|
private
|
|
167
203
|
|
|
168
204
|
def rpc_execute(rpc_name, input, **opts)
|
|
169
|
-
rpc_name, input_enc, output_enc, rpc_opts = @options.grpc_rpcs[rpc_name
|
|
205
|
+
rpc_name, input_enc, output_enc, rpc_opts = @options.grpc_rpcs[rpc_name]
|
|
170
206
|
|
|
171
207
|
exec_opts = rpc_opts.merge(opts)
|
|
172
208
|
|
|
@@ -230,16 +266,6 @@ module HTTPX
|
|
|
230
266
|
|
|
231
267
|
build_request(:post, uri, headers: headers, body: body)
|
|
232
268
|
end
|
|
233
|
-
|
|
234
|
-
def respond_to_missing?(meth, *, &blk)
|
|
235
|
-
@options.grpc_rpcs.key?(meth.to_s) || super
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def method_missing(meth, *args, **kwargs, &blk)
|
|
239
|
-
return rpc_execute(meth, *args, **kwargs, &blk) if @options.grpc_rpcs.key?(meth.to_s)
|
|
240
|
-
|
|
241
|
-
super
|
|
242
|
-
end
|
|
243
269
|
end
|
|
244
270
|
end
|
|
245
271
|
register_plugin :grpc, GRPC
|
data/lib/httpx/plugins/h2c.rb
CHANGED
|
@@ -24,15 +24,19 @@ module HTTPX
|
|
|
24
24
|
def call(connection, request, response)
|
|
25
25
|
connection.upgrade_to_h2c(request, response)
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
def extra_options(options)
|
|
29
|
+
options.merge(max_concurrent_requests: 1)
|
|
30
|
+
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
module InstanceMethods
|
|
30
|
-
def send_requests(*requests
|
|
34
|
+
def send_requests(*requests)
|
|
31
35
|
upgrade_request, *remainder = requests
|
|
32
36
|
|
|
33
37
|
return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
|
|
34
38
|
|
|
35
|
-
connection = pool.find_connection(upgrade_request.uri,
|
|
39
|
+
connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
|
|
36
40
|
|
|
37
41
|
return super if connection && connection.upgrade_protocol == :h2c
|
|
38
42
|
|
|
@@ -42,7 +46,7 @@ module HTTPX
|
|
|
42
46
|
upgrade_request.headers["upgrade"] = "h2c"
|
|
43
47
|
upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
|
|
44
48
|
|
|
45
|
-
super(upgrade_request, *remainder
|
|
49
|
+
super(upgrade_request, *remainder)
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
|
|
@@ -44,6 +44,11 @@ module HTTPX
|
|
|
44
44
|
meter_elapsed_time("Session: initialized!!!")
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def close(*)
|
|
48
|
+
super
|
|
49
|
+
meter_elapsed_time("Session -> close")
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
private
|
|
48
53
|
|
|
49
54
|
def build_requests(*)
|
|
@@ -55,11 +60,6 @@ module HTTPX
|
|
|
55
60
|
meter_elapsed_time("Session -> response") if response
|
|
56
61
|
response
|
|
57
62
|
end
|
|
58
|
-
|
|
59
|
-
def close(*)
|
|
60
|
-
super
|
|
61
|
-
meter_elapsed_time("Session -> close")
|
|
62
|
-
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
module RequestMethods
|
|
@@ -69,9 +69,9 @@ module HTTPX
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def transition(nextstate)
|
|
72
|
-
|
|
72
|
+
prev_state = @state
|
|
73
73
|
super
|
|
74
|
-
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{
|
|
74
|
+
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{prev_state}] -> #{@state}") if prev_state != @state
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -84,7 +84,7 @@ module HTTPX
|
|
|
84
84
|
def transition(nextstate)
|
|
85
85
|
state = @state
|
|
86
86
|
super
|
|
87
|
-
meter_elapsed_time("Connection[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
|
87
|
+
meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "delegate"
|
|
5
|
+
|
|
6
|
+
module HTTPX::Plugins
|
|
7
|
+
module Multipart
|
|
8
|
+
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
9
|
+
|
|
10
|
+
CRLF = "\r\n"
|
|
11
|
+
|
|
12
|
+
class FilePart < SimpleDelegator
|
|
13
|
+
attr_reader :original_filename, :content_type
|
|
14
|
+
|
|
15
|
+
def initialize(filename, content_type)
|
|
16
|
+
@original_filename = filename
|
|
17
|
+
@content_type = content_type
|
|
18
|
+
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
19
|
+
super(@file)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
|
24
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
|
25
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i.freeze
|
|
26
|
+
BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i.freeze
|
|
27
|
+
BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i.freeze
|
|
28
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
|
29
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
|
30
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
|
31
|
+
# Updated definitions from RFC 2231
|
|
32
|
+
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}.freeze
|
|
33
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/.freeze
|
|
34
|
+
SECTION = /\*[0-9]+/.freeze
|
|
35
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/.freeze
|
|
36
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/.freeze
|
|
37
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/.freeze
|
|
38
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/.freeze
|
|
39
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/.freeze
|
|
40
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/.freeze
|
|
41
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/.freeze
|
|
42
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/.freeze
|
|
43
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/.freeze
|
|
44
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/.freeze
|
|
45
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i.freeze
|
|
46
|
+
|
|
47
|
+
class Decoder
|
|
48
|
+
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
|
49
|
+
WINDOW_SIZE = 2 << 14
|
|
50
|
+
|
|
51
|
+
def initialize(response)
|
|
52
|
+
@boundary = begin
|
|
53
|
+
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
|
54
|
+
raise Error, "no boundary declared in content-type header" unless m
|
|
55
|
+
|
|
56
|
+
m.strip
|
|
57
|
+
end
|
|
58
|
+
@buffer = "".b
|
|
59
|
+
@parts = {}
|
|
60
|
+
@intermediate_boundary = "--#{@boundary}"
|
|
61
|
+
@state = :idle
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(response, _)
|
|
65
|
+
response.body.each do |chunk|
|
|
66
|
+
@buffer << chunk
|
|
67
|
+
|
|
68
|
+
parse
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
|
72
|
+
|
|
73
|
+
@parts
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def parse
|
|
79
|
+
case @state
|
|
80
|
+
when :idle
|
|
81
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
|
82
|
+
|
|
83
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
|
84
|
+
|
|
85
|
+
@state = :part_header
|
|
86
|
+
when :part_header
|
|
87
|
+
idx = @buffer.index("#{CRLF}#{CRLF}")
|
|
88
|
+
|
|
89
|
+
# raise Error, "couldn't parse part headers" unless idx
|
|
90
|
+
return unless idx
|
|
91
|
+
|
|
92
|
+
head = @buffer.byteslice(0..idx + 4 - 1)
|
|
93
|
+
|
|
94
|
+
@buffer = @buffer.byteslice(head.bytesize..-1)
|
|
95
|
+
|
|
96
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
97
|
+
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
|
98
|
+
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
|
99
|
+
name.gsub!(/\\(.)/, "\\1")
|
|
100
|
+
name
|
|
101
|
+
else
|
|
102
|
+
name = head[MULTIPART_CONTENT_ID, 1]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
filename = get_filename(head)
|
|
106
|
+
|
|
107
|
+
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
|
108
|
+
|
|
109
|
+
@current = name
|
|
110
|
+
|
|
111
|
+
@parts[name] = if filename
|
|
112
|
+
FilePart.new(filename, content_type)
|
|
113
|
+
else
|
|
114
|
+
"".b
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@state = :part_body
|
|
118
|
+
when :part_body
|
|
119
|
+
part = @parts[@current]
|
|
120
|
+
|
|
121
|
+
body_separator = if part.is_a?(FilePart)
|
|
122
|
+
"#{CRLF}#{CRLF}"
|
|
123
|
+
else
|
|
124
|
+
CRLF
|
|
125
|
+
end
|
|
126
|
+
idx = @buffer.index(body_separator)
|
|
127
|
+
|
|
128
|
+
if idx
|
|
129
|
+
payload = @buffer.byteslice(0..idx - 1)
|
|
130
|
+
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
|
131
|
+
part << payload
|
|
132
|
+
part.rewind if part.respond_to?(:rewind)
|
|
133
|
+
@state = :parse_boundary
|
|
134
|
+
else
|
|
135
|
+
part << @buffer
|
|
136
|
+
@buffer.clear
|
|
137
|
+
end
|
|
138
|
+
when :parse_boundary
|
|
139
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
|
140
|
+
|
|
141
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
|
142
|
+
|
|
143
|
+
if @buffer == "--"
|
|
144
|
+
@buffer.clear
|
|
145
|
+
@state = :done
|
|
146
|
+
return
|
|
147
|
+
elsif @buffer.start_with?(CRLF)
|
|
148
|
+
@buffer = @buffer.byteslice(2..-1)
|
|
149
|
+
@state = :part_header
|
|
150
|
+
else
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
when :done
|
|
154
|
+
raise Error, "parsing should have been over by now"
|
|
155
|
+
end until @buffer.empty?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def get_filename(head)
|
|
159
|
+
filename = nil
|
|
160
|
+
case head
|
|
161
|
+
when RFC2183
|
|
162
|
+
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
163
|
+
|
|
164
|
+
if (filename = params["filename"])
|
|
165
|
+
filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
|
|
166
|
+
elsif (filename = params["filename*"])
|
|
167
|
+
encoding, _, filename = filename.split("'", 3)
|
|
168
|
+
end
|
|
169
|
+
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
|
170
|
+
filename = Regexp.last_match(1)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return unless filename
|
|
174
|
+
|
|
175
|
+
filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
176
|
+
|
|
177
|
+
filename.scrub!
|
|
178
|
+
|
|
179
|
+
filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
|
|
180
|
+
|
|
181
|
+
filename.force_encoding ::Encoding.find(encoding) if encoding
|
|
182
|
+
|
|
183
|
+
filename
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
|
17
17
|
|
|
18
18
|
elsif defined?(MimeMagic)
|
|
19
19
|
|
|
20
|
-
def call(file,
|
|
20
|
+
def call(file, _)
|
|
21
21
|
mime = MimeMagic.by_magic(file)
|
|
22
22
|
mime.type if mime
|
|
23
23
|
end
|
|
@@ -25,7 +25,7 @@ module HTTPX
|
|
|
25
25
|
elsif system("which file", out: File::NULL)
|
|
26
26
|
require "open3"
|
|
27
27
|
|
|
28
|
-
def call(file,
|
|
28
|
+
def call(file, _)
|
|
29
29
|
return if file.eof? # file command returns "application/x-empty" for empty files
|
|
30
30
|
|
|
31
31
|
Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
|
@@ -56,7 +56,7 @@ module HTTPX
|
|
|
56
56
|
|
|
57
57
|
else
|
|
58
58
|
|
|
59
|
-
def call(
|
|
59
|
+
def call(_, _); end
|
|
60
60
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
@@ -8,7 +8,7 @@ module HTTPX
|
|
|
8
8
|
def call(value)
|
|
9
9
|
# take out specialized objects of the way
|
|
10
10
|
if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
|
|
11
|
-
return
|
|
11
|
+
return value, value.content_type, value.filename
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
content_type = filename = nil
|
|
@@ -19,7 +19,7 @@ module HTTPX
|
|
|
19
19
|
value = value[:body]
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
value = value.open(
|
|
22
|
+
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
|
23
23
|
|
|
24
24
|
if value.is_a?(File)
|
|
25
25
|
filename ||= File.basename(value.path)
|