httpx 0.10.0 → 0.11.2
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/README.md +11 -3
- data/doc/release_notes/0_10_1.md +37 -0
- data/doc/release_notes/0_10_2.md +5 -0
- data/doc/release_notes/0_11_0.md +76 -0
- data/doc/release_notes/0_11_1.md +1 -0
- data/doc/release_notes/0_11_2.md +5 -0
- data/lib/httpx/adapters/datadog.rb +205 -0
- data/lib/httpx/adapters/faraday.rb +0 -2
- data/lib/httpx/adapters/webmock.rb +123 -0
- data/lib/httpx/chainable.rb +8 -7
- data/lib/httpx/connection.rb +4 -15
- data/lib/httpx/connection/http1.rb +14 -1
- data/lib/httpx/connection/http2.rb +15 -16
- data/lib/httpx/domain_name.rb +1 -3
- data/lib/httpx/errors.rb +3 -1
- data/lib/httpx/headers.rb +1 -0
- data/lib/httpx/io/ssl.rb +4 -8
- data/lib/httpx/io/udp.rb +4 -3
- data/lib/httpx/plugins/compression.rb +1 -1
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
- data/lib/httpx/plugins/expect.rb +33 -8
- data/lib/httpx/plugins/multipart.rb +42 -23
- data/lib/httpx/plugins/multipart/encoder.rb +115 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
- data/lib/httpx/plugins/multipart/part.rb +34 -0
- data/lib/httpx/plugins/proxy.rb +16 -2
- data/lib/httpx/plugins/proxy/socks4.rb +14 -16
- data/lib/httpx/plugins/proxy/socks5.rb +3 -2
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/pool.rb +8 -14
- data/lib/httpx/request.rb +22 -12
- data/lib/httpx/resolver.rb +7 -6
- data/lib/httpx/resolver/https.rb +18 -23
- data/lib/httpx/resolver/native.rb +22 -19
- data/lib/httpx/resolver/resolver_mixin.rb +4 -2
- data/lib/httpx/resolver/system.rb +3 -3
- data/lib/httpx/selector.rb +9 -13
- data/lib/httpx/session.rb +24 -21
- data/lib/httpx/transcoder.rb +20 -0
- data/lib/httpx/transcoder/form.rb +9 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/connection.rbs +84 -1
- data/sig/connection/http1.rbs +66 -0
- data/sig/connection/http2.rbs +73 -0
- data/sig/headers.rbs +3 -0
- data/sig/httpx.rbs +1 -0
- data/sig/options.rbs +3 -3
- data/sig/plugins/basic_authentication.rbs +1 -1
- data/sig/plugins/compression.rbs +1 -1
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +1 -1
- data/sig/plugins/h2c.rbs +1 -1
- data/sig/plugins/multipart.rbs +29 -4
- data/sig/plugins/persistent.rbs +1 -1
- data/sig/plugins/proxy.rbs +2 -2
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/pool.rbs +36 -2
- data/sig/request.rbs +2 -2
- data/sig/resolver.rbs +26 -0
- data/sig/resolver/https.rbs +51 -0
- data/sig/resolver/native.rbs +60 -0
- data/sig/resolver/resolver_mixin.rbs +27 -0
- data/sig/resolver/system.rbs +17 -0
- data/sig/response.rbs +2 -2
- data/sig/selector.rbs +20 -0
- data/sig/session.rbs +3 -3
- data/sig/transcoder.rbs +4 -2
- data/sig/transcoder/body.rbs +2 -0
- data/sig/transcoder/form.rbs +8 -2
- data/sig/transcoder/json.rbs +3 -1
- metadata +47 -48
- data/lib/httpx/resolver/options.rb +0 -25
- data/sig/missing.rbs +0 -12
- data/sig/test.rbs +0 -9
data/lib/httpx/chainable.rb
CHANGED
@@ -42,12 +42,15 @@ module HTTPX
|
|
42
42
|
end
|
43
43
|
|
44
44
|
# deprecated
|
45
|
+
# :nocov:
|
45
46
|
def plugins(*args, **opts)
|
47
|
+
warn ":#{__method__} is deprecated, use :plugin instead"
|
46
48
|
klass = is_a?(Session) ? self.class : Session
|
47
49
|
klass = Class.new(klass)
|
48
50
|
klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
|
49
51
|
klass.plugins(*args, **opts).new
|
50
52
|
end
|
53
|
+
# :nocov:
|
51
54
|
|
52
55
|
def with(options, &blk)
|
53
56
|
branch(default_options.merge(options), &blk)
|
@@ -56,7 +59,7 @@ module HTTPX
|
|
56
59
|
private
|
57
60
|
|
58
61
|
def default_options
|
59
|
-
@options ||
|
62
|
+
@options || Session.default_options
|
60
63
|
end
|
61
64
|
|
62
65
|
def branch(options, &blk)
|
@@ -66,12 +69,10 @@ module HTTPX
|
|
66
69
|
end
|
67
70
|
|
68
71
|
def method_missing(meth, *args, **options)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
super
|
74
|
-
end
|
72
|
+
return super unless meth =~ /\Awith_(.+)/
|
73
|
+
|
74
|
+
option = Regexp.last_match(1).to_sym
|
75
|
+
with(option => (args.first || options))
|
75
76
|
end
|
76
77
|
|
77
78
|
def respond_to_missing?(meth, *args)
|
data/lib/httpx/connection.rb
CHANGED
@@ -120,8 +120,8 @@ module HTTPX
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
-
def create_idle
|
124
|
-
self.class.new(@type, @origin, @options)
|
123
|
+
def create_idle(options = {})
|
124
|
+
self.class.new(@type, @origin, @options.merge(options))
|
125
125
|
end
|
126
126
|
|
127
127
|
def merge(connection)
|
@@ -131,17 +131,6 @@ module HTTPX
|
|
131
131
|
end
|
132
132
|
end
|
133
133
|
|
134
|
-
def unmerge(connection)
|
135
|
-
@origins -= connection.instance_variable_get(:@origins)
|
136
|
-
purge_pending do |request|
|
137
|
-
request.uri.origin == connection.origin && begin
|
138
|
-
request.transition(:idle)
|
139
|
-
connection.send(request)
|
140
|
-
true
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
134
|
def purge_pending(&block)
|
146
135
|
pendings = []
|
147
136
|
if @parser
|
@@ -399,7 +388,7 @@ module HTTPX
|
|
399
388
|
parser.on(:error) do |request, ex|
|
400
389
|
case ex
|
401
390
|
when MisdirectedRequestError
|
402
|
-
emit(:
|
391
|
+
emit(:misdirected, request)
|
403
392
|
else
|
404
393
|
response = ErrorResponse.new(request, ex, @options)
|
405
394
|
request.emit(:response, response)
|
@@ -435,7 +424,7 @@ module HTTPX
|
|
435
424
|
remove_instance_variable(:@total_timeout)
|
436
425
|
end
|
437
426
|
|
438
|
-
@io.close
|
427
|
+
@io.close if @io
|
439
428
|
@read_buffer.clear
|
440
429
|
if @keep_alive_timer
|
441
430
|
@keep_alive_timer.cancel
|
@@ -21,6 +21,7 @@ module HTTPX
|
|
21
21
|
@version = [1, 1]
|
22
22
|
@pending = []
|
23
23
|
@requests = []
|
24
|
+
@handshake_completed = false
|
24
25
|
end
|
25
26
|
|
26
27
|
def interests
|
@@ -155,6 +156,7 @@ module HTTPX
|
|
155
156
|
@parser.reset!
|
156
157
|
@max_requests -= 1
|
157
158
|
manage_connection(response)
|
159
|
+
|
158
160
|
send(@pending.shift) unless @pending.empty?
|
159
161
|
end
|
160
162
|
|
@@ -182,17 +184,28 @@ module HTTPX
|
|
182
184
|
connection = response.headers["connection"]
|
183
185
|
case connection
|
184
186
|
when /keep-alive/i
|
187
|
+
if @handshake_completed
|
188
|
+
if @max_requests.zero?
|
189
|
+
@pending.concat(@requests)
|
190
|
+
@requests.clear
|
191
|
+
emit(:exhausted)
|
192
|
+
end
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
185
196
|
keep_alive = response.headers["keep-alive"]
|
186
197
|
return unless keep_alive
|
187
198
|
|
188
199
|
parameters = Hash[keep_alive.split(/ *, */).map do |pair|
|
189
200
|
pair.split(/ *= */)
|
190
201
|
end]
|
191
|
-
@max_requests = parameters["max"].to_i if parameters.key?("max")
|
202
|
+
@max_requests = parameters["max"].to_i - 1 if parameters.key?("max")
|
203
|
+
|
192
204
|
if parameters.key?("timeout")
|
193
205
|
keep_alive_timeout = parameters["timeout"].to_i
|
194
206
|
emit(:timeout, keep_alive_timeout)
|
195
207
|
end
|
208
|
+
@handshake_completed = true
|
196
209
|
when /close/i
|
197
210
|
disable
|
198
211
|
when nil
|
@@ -51,12 +51,8 @@ module HTTPX
|
|
51
51
|
:rw
|
52
52
|
end
|
53
53
|
|
54
|
-
def
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
def close(*args)
|
59
|
-
@connection.goaway(*args) unless @connection.state == :closed
|
54
|
+
def close
|
55
|
+
@connection.goaway unless @connection.state == :closed
|
60
56
|
emit(:close)
|
61
57
|
end
|
62
58
|
|
@@ -163,14 +159,17 @@ module HTTPX
|
|
163
159
|
@connection.send_connection_preface
|
164
160
|
end
|
165
161
|
|
162
|
+
alias_method :reset, :init_connection
|
163
|
+
public :reset
|
164
|
+
|
166
165
|
def handle_stream(stream, request)
|
167
|
-
stream.on(:close, &method(:on_stream_close).curry[stream, request])
|
166
|
+
stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
|
168
167
|
stream.on(:half_close) do
|
169
168
|
log(level: 2) { "#{stream.id}: waiting for response..." }
|
170
169
|
end
|
171
|
-
stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
|
172
|
-
stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
|
173
|
-
stream.on(:data, &method(:on_stream_data).curry[stream, request])
|
170
|
+
stream.on(:altsvc, &method(:on_altsvc).curry(2)[request.origin])
|
171
|
+
stream.on(:headers, &method(:on_stream_headers).curry(3)[stream, request])
|
172
|
+
stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
|
174
173
|
end
|
175
174
|
|
176
175
|
def join_headers(stream, request)
|
@@ -270,16 +269,16 @@ module HTTPX
|
|
270
269
|
end
|
271
270
|
|
272
271
|
def on_close(_last_frame, error, _payload)
|
272
|
+
is_connection_closed = @connection.state == :closed
|
273
273
|
if error && error != :no_error
|
274
|
+
@buffer.clear if is_connection_closed
|
274
275
|
ex = Error.new(0, error)
|
275
276
|
ex.set_backtrace(caller)
|
276
|
-
|
277
|
-
emit(:error, request, ex)
|
278
|
-
end
|
277
|
+
handle_error(ex)
|
279
278
|
end
|
280
|
-
return unless
|
279
|
+
return unless is_connection_closed && @streams.size.zero?
|
281
280
|
|
282
|
-
emit(:close)
|
281
|
+
emit(:close, is_connection_closed)
|
283
282
|
end
|
284
283
|
|
285
284
|
def on_frame_sent(frame)
|
@@ -317,7 +316,7 @@ module HTTPX
|
|
317
316
|
end
|
318
317
|
|
319
318
|
def on_pong(ping)
|
320
|
-
if !@pings.delete(ping)
|
319
|
+
if !@pings.delete(ping.to_s)
|
321
320
|
close(:protocol_error, "ping payload did not match")
|
322
321
|
else
|
323
322
|
emit(:pong)
|
data/lib/httpx/domain_name.rb
CHANGED
@@ -139,10 +139,8 @@ module HTTPX
|
|
139
139
|
elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
|
140
140
|
# The other is higher
|
141
141
|
-1
|
142
|
-
elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT
|
143
|
-
# The other is lower
|
144
|
-
1
|
145
142
|
else
|
143
|
+
# The other is lower
|
146
144
|
1
|
147
145
|
end
|
148
146
|
end
|
data/lib/httpx/errors.rb
CHANGED
@@ -24,6 +24,8 @@ module HTTPX
|
|
24
24
|
|
25
25
|
ConnectTimeoutError = Class.new(TimeoutError)
|
26
26
|
|
27
|
+
ResolveTimeoutError = Class.new(TimeoutError)
|
28
|
+
|
27
29
|
ResolveError = Class.new(Error)
|
28
30
|
|
29
31
|
NativeResolveError = Class.new(ResolveError) do
|
@@ -41,7 +43,7 @@ module HTTPX
|
|
41
43
|
|
42
44
|
def initialize(response)
|
43
45
|
@response = response
|
44
|
-
super("HTTP Error: #{@response.status}")
|
46
|
+
super("HTTP Error: #{@response.status} #{@response.headers}\n#{@response.body}")
|
45
47
|
end
|
46
48
|
|
47
49
|
def status
|
data/lib/httpx/headers.rb
CHANGED
data/lib/httpx/io/ssl.rb
CHANGED
@@ -7,16 +7,16 @@ module HTTPX
|
|
7
7
|
TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
|
8
8
|
{ alpn_protocols: %w[h2 http/1.1] }
|
9
9
|
else
|
10
|
-
# :nocov:
|
11
10
|
{}
|
12
|
-
# :nocov:
|
13
11
|
end
|
14
12
|
|
15
13
|
def initialize(_, _, options)
|
16
14
|
@ctx = OpenSSL::SSL::SSLContext.new
|
17
15
|
ctx_options = TLS_OPTIONS.merge(options.ssl)
|
16
|
+
@tls_hostname = ctx_options.delete(:hostname)
|
18
17
|
@ctx.set_params(ctx_options) unless ctx_options.empty?
|
19
18
|
super
|
19
|
+
@tls_hostname ||= @hostname
|
20
20
|
@state = :negotiated if @keep_open
|
21
21
|
end
|
22
22
|
|
@@ -59,11 +59,11 @@ module HTTPX
|
|
59
59
|
|
60
60
|
unless @io.is_a?(OpenSSL::SSL::SSLSocket)
|
61
61
|
@io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
|
62
|
-
@io.hostname = @
|
62
|
+
@io.hostname = @tls_hostname
|
63
63
|
@io.sync_close = true
|
64
64
|
end
|
65
65
|
@io.connect_nonblock
|
66
|
-
@io.post_connection_check(@
|
66
|
+
@io.post_connection_check(@tls_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
|
67
67
|
transition(:negotiated)
|
68
68
|
rescue ::IO::WaitReadable
|
69
69
|
@interests = :r
|
@@ -71,7 +71,6 @@ module HTTPX
|
|
71
71
|
@interests = :w
|
72
72
|
end
|
73
73
|
|
74
|
-
# :nocov:
|
75
74
|
if RUBY_VERSION < "2.3"
|
76
75
|
def read(_, buffer)
|
77
76
|
super
|
@@ -99,14 +98,11 @@ module HTTPX
|
|
99
98
|
end
|
100
99
|
end
|
101
100
|
end
|
102
|
-
# :nocov:
|
103
101
|
|
104
|
-
# :nocov:
|
105
102
|
def inspect
|
106
103
|
id = @io.closed? ? "closed" : @io.to_io.fileno
|
107
104
|
"#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
|
108
105
|
end
|
109
|
-
# :nocov:
|
110
106
|
|
111
107
|
private
|
112
108
|
|
data/lib/httpx/io/udp.rb
CHANGED
@@ -25,7 +25,7 @@ module HTTPX
|
|
25
25
|
true
|
26
26
|
end
|
27
27
|
|
28
|
-
if RUBY_VERSION < "2.
|
28
|
+
if RUBY_VERSION < "2.3"
|
29
29
|
# :nocov:
|
30
30
|
def close
|
31
31
|
@io.close
|
@@ -40,14 +40,15 @@ module HTTPX
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def write(buffer)
|
43
|
-
siz = @io.send(buffer, 0, @host, @port)
|
43
|
+
siz = @io.send(buffer.to_s, 0, @host, @port)
|
44
44
|
log { "WRITE: #{siz} bytes..." }
|
45
45
|
buffer.shift!(siz)
|
46
46
|
siz
|
47
47
|
end
|
48
48
|
|
49
49
|
# :nocov:
|
50
|
-
if
|
50
|
+
if (RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "21.1.0") ||
|
51
|
+
RUBY_VERSION < "2.3"
|
51
52
|
def read(size, buffer)
|
52
53
|
data, _ = @io.recvfrom_nonblock(size)
|
53
54
|
buffer.replace(data)
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -10,6 +10,10 @@ module HTTPX
|
|
10
10
|
module Expect
|
11
11
|
EXPECT_TIMEOUT = 2
|
12
12
|
|
13
|
+
def self.no_expect_store
|
14
|
+
@no_expect_store ||= []
|
15
|
+
end
|
16
|
+
|
13
17
|
def self.extra_options(options)
|
14
18
|
Class.new(options.class) do
|
15
19
|
def_option(:expect_timeout) do |seconds|
|
@@ -28,25 +32,46 @@ module HTTPX
|
|
28
32
|
end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
|
29
33
|
end
|
30
34
|
|
31
|
-
module
|
32
|
-
def initialize(
|
35
|
+
module RequestMethods
|
36
|
+
def initialize(*)
|
33
37
|
super
|
34
|
-
return if @body.
|
38
|
+
return if @body.empty?
|
35
39
|
|
36
|
-
threshold = options.expect_threshold_size
|
37
|
-
return if threshold &&
|
40
|
+
threshold = @options.expect_threshold_size
|
41
|
+
return if threshold && !@body.unbounded_body? && @body.bytesize < threshold
|
42
|
+
|
43
|
+
return if Expect.no_expect_store.include?(origin)
|
38
44
|
|
39
45
|
@headers["expect"] = "100-continue"
|
40
46
|
end
|
47
|
+
|
48
|
+
def response=(response)
|
49
|
+
if response && response.status == 100 &&
|
50
|
+
!@headers.key?("expect") &&
|
51
|
+
(@state == :body || @state == :done)
|
52
|
+
|
53
|
+
# if we're past this point, this means that we just received a 100-Continue response,
|
54
|
+
# but the request doesn't have the expect flag, and is already flushing (or flushed) the body.
|
55
|
+
#
|
56
|
+
# this means that expect was deactivated for this request too soon, i.e. response took longer.
|
57
|
+
#
|
58
|
+
# so we have to reactivate it again.
|
59
|
+
@headers["expect"] = "100-continue"
|
60
|
+
@informational_status = 100
|
61
|
+
Expect.no_expect_store.delete(origin)
|
62
|
+
end
|
63
|
+
super
|
64
|
+
end
|
41
65
|
end
|
42
66
|
|
43
67
|
module ConnectionMethods
|
44
68
|
def send(request)
|
45
|
-
request.once(:
|
69
|
+
request.once(:expect) do
|
46
70
|
@timers.after(@options.expect_timeout) do
|
47
|
-
if request.state == :
|
71
|
+
if request.state == :expect && !request.expects?
|
72
|
+
Expect.no_expect_store << request.origin
|
48
73
|
request.headers.delete("expect")
|
49
|
-
|
74
|
+
consume
|
50
75
|
end
|
51
76
|
end
|
52
77
|
end
|
@@ -10,38 +10,57 @@ module HTTPX
|
|
10
10
|
# https://gitlab.com/honeyryderchuck/httpx/wikis/Multipart-Uploads
|
11
11
|
#
|
12
12
|
module Multipart
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def_delegator :@raw, :content_type
|
20
|
-
|
21
|
-
def_delegator :@raw, :to_s
|
22
|
-
|
23
|
-
def_delegator :@raw, :read
|
13
|
+
MULTIPART_VALUE_COND = lambda do |value|
|
14
|
+
value.respond_to?(:read) ||
|
15
|
+
(value.respond_to?(:to_hash) &&
|
16
|
+
value.key?(:body) &&
|
17
|
+
(value.key?(:filename) || value.key?(:content_type)))
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
class << self
|
21
|
+
def normalize_keys(key, value, &block)
|
22
|
+
Transcoder.normalize_keys(key, value, MULTIPART_VALUE_COND, &block)
|
23
|
+
end
|
28
24
|
|
29
|
-
|
30
|
-
|
25
|
+
def load_dependencies(*)
|
26
|
+
begin
|
27
|
+
unless defined?(HTTP::FormData)
|
28
|
+
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
29
|
+
require "http/form_data"
|
30
|
+
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
31
|
+
"https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
32
|
+
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
33
|
+
end
|
34
|
+
rescue LoadError
|
31
35
|
end
|
36
|
+
require "httpx/plugins/multipart/encoder"
|
37
|
+
require "httpx/plugins/multipart/part"
|
38
|
+
require "httpx/plugins/multipart/mime_type_detector"
|
32
39
|
end
|
33
40
|
|
34
|
-
def
|
35
|
-
|
41
|
+
def configure(*)
|
42
|
+
Transcoder.register("form", FormTranscoder)
|
36
43
|
end
|
37
44
|
end
|
38
45
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
46
|
+
module FormTranscoder
|
47
|
+
module_function
|
42
48
|
|
43
|
-
|
44
|
-
|
49
|
+
def encode(form)
|
50
|
+
if multipart?(form)
|
51
|
+
Encoder.new(form)
|
52
|
+
else
|
53
|
+
Transcoder::Form::Encoder.new(form)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def multipart?(data)
|
58
|
+
data.any? do |_, v|
|
59
|
+
MULTIPART_VALUE_COND.call(v) ||
|
60
|
+
(v.respond_to?(:to_ary) && v.to_ary.any?(&MULTIPART_VALUE_COND)) ||
|
61
|
+
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| MULTIPART_VALUE_COND.call(e) })
|
62
|
+
end
|
63
|
+
end
|
45
64
|
end
|
46
65
|
end
|
47
66
|
register_plugin :multipart, Multipart
|