httpx 0.10.2 → 0.11.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/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
|