httpx 0.10.2 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -3
- data/doc/release_notes/0_11_0.md +76 -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 +1 -1
- data/lib/httpx/connection/http2.rb +4 -4
- data/lib/httpx/domain_name.rb +1 -3
- data/lib/httpx/errors.rb +2 -0
- data/lib/httpx/headers.rb +1 -0
- data/lib/httpx/io/ssl.rb +4 -8
- data/lib/httpx/io/udp.rb +1 -1
- data/lib/httpx/plugins/expect.rb +33 -8
- data/lib/httpx/plugins/multipart.rb +40 -35
- 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/socks5.rb +3 -2
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/request.rb +21 -11
- data/lib/httpx/resolver.rb +7 -4
- data/lib/httpx/resolver/https.rb +4 -2
- data/lib/httpx/resolver/native.rb +10 -6
- data/lib/httpx/resolver/system.rb +1 -1
- data/lib/httpx/selector.rb +1 -0
- data/lib/httpx/session.rb +15 -18
- data/lib/httpx/transcoder.rb +6 -4
- data/lib/httpx/version.rb +1 -1
- data/sig/connection/http2.rbs +3 -4
- data/sig/headers.rbs +3 -0
- data/sig/plugins/multipart.rbs +27 -4
- data/sig/request.rbs +1 -1
- data/sig/resolver/https.rbs +2 -0
- data/sig/response.rbs +1 -1
- data/sig/session.rbs +1 -1
- data/sig/transcoder.rbs +2 -2
- data/sig/transcoder/body.rbs +2 -0
- data/sig/transcoder/form.rbs +7 -1
- data/sig/transcoder/json.rbs +3 -1
- metadata +9 -23
- data/sig/missing.rbs +0 -12
data/lib/httpx/chainable.rb
CHANGED
@@ -163,13 +163,13 @@ module HTTPX
|
|
163
163
|
public :reset
|
164
164
|
|
165
165
|
def handle_stream(stream, request)
|
166
|
-
stream.on(:close, &method(:on_stream_close).curry[stream, request])
|
166
|
+
stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
|
167
167
|
stream.on(:half_close) do
|
168
168
|
log(level: 2) { "#{stream.id}: waiting for response..." }
|
169
169
|
end
|
170
|
-
stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
|
171
|
-
stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
|
172
|
-
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])
|
173
173
|
end
|
174
174
|
|
175
175
|
def join_headers(stream, request)
|
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
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
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,53 +10,58 @@ 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
|
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
|
22
19
|
|
23
|
-
|
20
|
+
class << self
|
21
|
+
def normalize_keys(key, value, &block)
|
22
|
+
Transcoder.normalize_keys(key, value, MULTIPART_VALUE_COND, &block)
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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."
|
30
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"
|
39
|
+
end
|
32
40
|
|
33
|
-
|
34
|
-
|
35
|
-
|
41
|
+
def configure(*)
|
42
|
+
Transcoder.register("form", FormTranscoder)
|
43
|
+
end
|
44
|
+
end
|
36
45
|
|
37
|
-
|
46
|
+
module FormTranscoder
|
47
|
+
module_function
|
38
48
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
49
|
+
def encode(form)
|
50
|
+
if multipart?(form)
|
51
|
+
Encoder.new(form)
|
52
|
+
else
|
53
|
+
Transcoder::Form::Encoder.new(form)
|
45
54
|
end
|
46
55
|
end
|
47
56
|
|
48
|
-
def
|
49
|
-
|
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
|
50
63
|
end
|
51
64
|
end
|
52
|
-
|
53
|
-
def self.load_dependencies(*)
|
54
|
-
require "http/form_data"
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.configure(*)
|
58
|
-
Transcoder.register("form", FormTranscoder)
|
59
|
-
end
|
60
65
|
end
|
61
66
|
register_plugin :multipart, Multipart
|
62
67
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX::Plugins
|
4
|
+
module Multipart
|
5
|
+
class Encoder
|
6
|
+
attr_reader :bytesize
|
7
|
+
|
8
|
+
def initialize(form)
|
9
|
+
@boundary = ("-" * 21) << SecureRandom.hex(21)
|
10
|
+
@part_index = 0
|
11
|
+
@buffer = "".b
|
12
|
+
|
13
|
+
@form = form
|
14
|
+
@parts = to_parts(form)
|
15
|
+
end
|
16
|
+
|
17
|
+
def content_type
|
18
|
+
"multipart/form-data; boundary=#{@boundary}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(length = nil, outbuf = nil)
|
22
|
+
data = outbuf.clear.force_encoding(Encoding::BINARY) if outbuf
|
23
|
+
data ||= "".b
|
24
|
+
|
25
|
+
read_chunks(data, length)
|
26
|
+
|
27
|
+
data unless length && data.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def rewind
|
31
|
+
form = @form.each_with_object([]) do |(key, val), aux|
|
32
|
+
v = case val
|
33
|
+
when File
|
34
|
+
val = val.reopen(val.path, File::RDONLY) if val.closed?
|
35
|
+
val.rewind
|
36
|
+
val
|
37
|
+
else
|
38
|
+
v
|
39
|
+
end
|
40
|
+
aux << [key, v]
|
41
|
+
end
|
42
|
+
@form = form
|
43
|
+
@parts = to_parts(form)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def to_parts(form)
|
49
|
+
@bytesize = 0
|
50
|
+
params = form.each_with_object([]) do |(key, val), aux|
|
51
|
+
Multipart.normalize_keys(key, val) do |k, v|
|
52
|
+
next if v.nil?
|
53
|
+
|
54
|
+
value, content_type, filename = Part.call(v)
|
55
|
+
|
56
|
+
header = header_part(k, content_type, filename)
|
57
|
+
@bytesize += header.size
|
58
|
+
aux << header
|
59
|
+
|
60
|
+
@bytesize += value.size
|
61
|
+
aux << value
|
62
|
+
|
63
|
+
delimiter = StringIO.new("\r\n")
|
64
|
+
@bytesize += delimiter.size
|
65
|
+
aux << delimiter
|
66
|
+
end
|
67
|
+
end
|
68
|
+
final_delimiter = StringIO.new("--#{@boundary}--\r\n")
|
69
|
+
@bytesize += final_delimiter.size
|
70
|
+
params << final_delimiter
|
71
|
+
|
72
|
+
params
|
73
|
+
end
|
74
|
+
|
75
|
+
def header_part(key, content_type, filename)
|
76
|
+
header = "--#{@boundary}\r\n".b
|
77
|
+
header << "Content-Disposition: form-data; name=#{key.inspect}".b
|
78
|
+
header << "; filename=#{filename.inspect}" if filename
|
79
|
+
header << "\r\nContent-Type: #{content_type}\r\n\r\n"
|
80
|
+
StringIO.new(header)
|
81
|
+
end
|
82
|
+
|
83
|
+
def read_chunks(buffer, length = nil)
|
84
|
+
while @part_index < @parts.size
|
85
|
+
chunk = read_from_part(length)
|
86
|
+
|
87
|
+
next unless chunk
|
88
|
+
|
89
|
+
buffer << chunk.force_encoding(Encoding::BINARY)
|
90
|
+
|
91
|
+
next unless length
|
92
|
+
|
93
|
+
length -= chunk.bytesize
|
94
|
+
|
95
|
+
break if length.zero?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# if there's a current part to read from, tries to read a chunk.
|
100
|
+
def read_from_part(max_length = nil)
|
101
|
+
part = @parts[@part_index]
|
102
|
+
|
103
|
+
chunk = part.read(max_length, @buffer)
|
104
|
+
|
105
|
+
return chunk if chunk && !chunk.empty?
|
106
|
+
|
107
|
+
part.close if part.respond_to?(:close)
|
108
|
+
|
109
|
+
@part_index += 1
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins::Multipart
|
5
|
+
module MimeTypeDetector
|
6
|
+
module_function
|
7
|
+
|
8
|
+
DEFAULT_MIMETYPE = "application/octet-stream"
|
9
|
+
|
10
|
+
# inspired by https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/determine_mime_type.rb
|
11
|
+
if defined?(MIME::Types)
|
12
|
+
|
13
|
+
def call(_file, filename)
|
14
|
+
mime = MIME::Types.of(filename).first
|
15
|
+
mime.content_type if mime
|
16
|
+
end
|
17
|
+
|
18
|
+
elsif defined?(MimeMagic)
|
19
|
+
|
20
|
+
def call(file, *)
|
21
|
+
mime = MimeMagic.by_magic(file)
|
22
|
+
mime.type if mime
|
23
|
+
end
|
24
|
+
|
25
|
+
elsif system("which file", out: File::NULL)
|
26
|
+
require "open3"
|
27
|
+
|
28
|
+
def call(file, *)
|
29
|
+
return if file.eof? # file command returns "application/x-empty" for empty files
|
30
|
+
|
31
|
+
Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
32
|
+
begin
|
33
|
+
::IO.copy_stream(file, stdin.binmode)
|
34
|
+
rescue Errno::EPIPE
|
35
|
+
end
|
36
|
+
file.rewind
|
37
|
+
stdin.close
|
38
|
+
|
39
|
+
status = thread.value
|
40
|
+
|
41
|
+
# call to file command failed
|
42
|
+
if status.nil? || !status.success?
|
43
|
+
$stderr.print(stderr.read)
|
44
|
+
else
|
45
|
+
|
46
|
+
output = stdout.read.strip
|
47
|
+
|
48
|
+
if output.include?("cannot open")
|
49
|
+
$stderr.print(output)
|
50
|
+
else
|
51
|
+
output
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
else
|
58
|
+
|
59
|
+
def call(*); end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|