httpx 0.10.2 → 0.12.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 +13 -5
- data/doc/release_notes/0_11_0.md +76 -0
- data/doc/release_notes/0_11_1.md +5 -0
- data/doc/release_notes/0_11_2.md +5 -0
- data/doc/release_notes/0_11_3.md +5 -0
- data/doc/release_notes/0_12_0.md +55 -0
- data/lib/httpx.rb +2 -1
- data/lib/httpx/adapters/datadog.rb +205 -0
- data/lib/httpx/adapters/faraday.rb +4 -8
- data/lib/httpx/adapters/webmock.rb +123 -0
- data/lib/httpx/altsvc.rb +1 -0
- data/lib/httpx/chainable.rb +1 -1
- data/lib/httpx/connection.rb +63 -15
- data/lib/httpx/connection/http1.rb +16 -5
- data/lib/httpx/connection/http2.rb +36 -29
- 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.rb +16 -3
- data/lib/httpx/io/ssl.rb +7 -13
- data/lib/httpx/io/tcp.rb +9 -8
- data/lib/httpx/io/tls.rb +218 -0
- data/lib/httpx/io/tls/box.rb +365 -0
- data/lib/httpx/io/tls/context.rb +199 -0
- data/lib/httpx/io/tls/ffi.rb +390 -0
- data/lib/httpx/io/udp.rb +4 -3
- data/lib/httpx/parser/http1.rb +4 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
- data/lib/httpx/plugins/aws_sigv4.rb +218 -0
- data/lib/httpx/plugins/compression.rb +1 -1
- data/lib/httpx/plugins/compression/deflate.rb +2 -5
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
- data/lib/httpx/plugins/expect.rb +33 -8
- data/lib/httpx/plugins/internal_telemetry.rb +93 -0
- data/lib/httpx/plugins/multipart.rb +42 -35
- data/lib/httpx/plugins/multipart/encoder.rb +110 -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 +1 -1
- data/lib/httpx/plugins/proxy/http.rb +1 -1
- data/lib/httpx/plugins/proxy/socks4.rb +8 -0
- data/lib/httpx/plugins/proxy/socks5.rb +11 -2
- data/lib/httpx/plugins/push_promise.rb +5 -4
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/stream.rb +3 -5
- data/lib/httpx/pool.rb +0 -1
- data/lib/httpx/registry.rb +1 -7
- data/lib/httpx/request.rb +32 -12
- data/lib/httpx/resolver.rb +7 -4
- data/lib/httpx/resolver/https.rb +7 -13
- data/lib/httpx/resolver/native.rb +10 -6
- data/lib/httpx/resolver/system.rb +1 -1
- data/lib/httpx/response.rb +9 -2
- data/lib/httpx/selector.rb +6 -0
- data/lib/httpx/session.rb +40 -20
- data/lib/httpx/transcoder.rb +6 -4
- data/lib/httpx/transcoder/body.rb +3 -5
- data/lib/httpx/version.rb +1 -1
- data/sig/connection/http1.rbs +2 -2
- data/sig/connection/http2.rbs +8 -7
- data/sig/headers.rbs +3 -0
- data/sig/plugins/aws_sdk_authentication.rbs +17 -0
- data/sig/plugins/aws_sigv4.rbs +65 -0
- data/sig/plugins/multipart.rbs +27 -4
- data/sig/plugins/push_promise.rbs +1 -1
- 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 +50 -47
- data/sig/missing.rbs +0 -12
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# The InternalTelemetry plugin is for internal use only. It is therefore undocumented, and
|
7
|
+
# its use is disencouraged, as API compatiblity will **not be guaranteed**.
|
8
|
+
#
|
9
|
+
# The gist of it is: when debug_level of logger is enabled to 3 or greater, considered internal-only
|
10
|
+
# supported log levels, it'll be loaded by default.
|
11
|
+
#
|
12
|
+
# Against a specific point of time, which will be by default the session initialization, but can be set
|
13
|
+
# by the end user in $http_init_time, different diff metrics can be shown. The "point of time" is calculated
|
14
|
+
# using the monotonic clock.
|
15
|
+
module InternalTelemetry
|
16
|
+
module TrackTimeMethods
|
17
|
+
private
|
18
|
+
|
19
|
+
def elapsed_time
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
meter_elapsed_time("#{self.class.superclass}##{caller_locations(1, 1)[0].label}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def meter_elapsed_time(label)
|
26
|
+
$http_init_time ||= Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
27
|
+
prev_time = $http_init_time
|
28
|
+
after_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
29
|
+
# $http_init_time = after_time
|
30
|
+
elapsed = after_time - prev_time
|
31
|
+
warn(+"\e[31m" << "[ELAPSED TIME]: #{label}: #{elapsed} (ms)" << "\e[0m")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
def self.included(klass)
|
37
|
+
klass.prepend TrackTimeMethods
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(*)
|
42
|
+
meter_elapsed_time("Session: initializing...")
|
43
|
+
super
|
44
|
+
meter_elapsed_time("Session: initialized!!!")
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def build_requests(*)
|
50
|
+
elapsed_time { super }
|
51
|
+
end
|
52
|
+
|
53
|
+
def fetch_response(*)
|
54
|
+
response = super
|
55
|
+
meter_elapsed_time("Session -> response") if response
|
56
|
+
response
|
57
|
+
end
|
58
|
+
|
59
|
+
def close(*)
|
60
|
+
super
|
61
|
+
meter_elapsed_time("Session -> close")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module RequestMethods
|
66
|
+
def self.included(klass)
|
67
|
+
klass.prepend TrackTimeMethods
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
def transition(nextstate)
|
72
|
+
state = @state
|
73
|
+
super
|
74
|
+
meter_elapsed_time("Request[#{@verb} #{@uri}: #{state}] -> #{nextstate}") if nextstate == @state
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module ConnectionMethods
|
79
|
+
def self.included(klass)
|
80
|
+
klass.prepend TrackTimeMethods
|
81
|
+
super
|
82
|
+
end
|
83
|
+
|
84
|
+
def transition(nextstate)
|
85
|
+
state = @state
|
86
|
+
super
|
87
|
+
meter_elapsed_time("Connection[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
register_plugin :internal_telemetry, InternalTelemetry
|
92
|
+
end
|
93
|
+
end
|
@@ -10,53 +10,60 @@ 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
|
+
# :nocov:
|
27
|
+
begin
|
28
|
+
unless defined?(HTTP::FormData)
|
29
|
+
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
30
|
+
require "http/form_data"
|
31
|
+
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
32
|
+
"https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
33
|
+
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
30
34
|
end
|
35
|
+
rescue LoadError
|
31
36
|
end
|
37
|
+
# :nocov:
|
38
|
+
require "httpx/plugins/multipart/encoder"
|
39
|
+
require "httpx/plugins/multipart/part"
|
40
|
+
require "httpx/plugins/multipart/mime_type_detector"
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
43
|
+
def configure(*)
|
44
|
+
Transcoder.register("form", FormTranscoder)
|
45
|
+
end
|
46
|
+
end
|
36
47
|
|
37
|
-
|
48
|
+
module FormTranscoder
|
49
|
+
module_function
|
38
50
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
51
|
+
def encode(form)
|
52
|
+
if multipart?(form)
|
53
|
+
Encoder.new(form)
|
54
|
+
else
|
55
|
+
Transcoder::Form::Encoder.new(form)
|
45
56
|
end
|
46
57
|
end
|
47
58
|
|
48
|
-
def
|
49
|
-
|
59
|
+
def multipart?(data)
|
60
|
+
data.any? do |_, v|
|
61
|
+
MULTIPART_VALUE_COND.call(v) ||
|
62
|
+
(v.respond_to?(:to_ary) && v.to_ary.any?(&MULTIPART_VALUE_COND)) ||
|
63
|
+
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| MULTIPART_VALUE_COND.call(e) })
|
64
|
+
end
|
50
65
|
end
|
51
66
|
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
67
|
end
|
61
68
|
register_plugin :multipart, Multipart
|
62
69
|
end
|
@@ -0,0 +1,110 @@
|
|
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
|
+
val = val.reopen(val.path, File::RDONLY) if val.is_a?(File) && val.closed?
|
33
|
+
val.rewind if val.respond_to?(:rewind)
|
34
|
+
aux << [key, val]
|
35
|
+
end
|
36
|
+
@form = form
|
37
|
+
@parts = to_parts(form)
|
38
|
+
@part_index = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def to_parts(form)
|
44
|
+
@bytesize = 0
|
45
|
+
params = form.each_with_object([]) do |(key, val), aux|
|
46
|
+
Multipart.normalize_keys(key, val) do |k, v|
|
47
|
+
next if v.nil?
|
48
|
+
|
49
|
+
value, content_type, filename = Part.call(v)
|
50
|
+
|
51
|
+
header = header_part(k, content_type, filename)
|
52
|
+
@bytesize += header.size
|
53
|
+
aux << header
|
54
|
+
|
55
|
+
@bytesize += value.size
|
56
|
+
aux << value
|
57
|
+
|
58
|
+
delimiter = StringIO.new("\r\n")
|
59
|
+
@bytesize += delimiter.size
|
60
|
+
aux << delimiter
|
61
|
+
end
|
62
|
+
end
|
63
|
+
final_delimiter = StringIO.new("--#{@boundary}--\r\n")
|
64
|
+
@bytesize += final_delimiter.size
|
65
|
+
params << final_delimiter
|
66
|
+
|
67
|
+
params
|
68
|
+
end
|
69
|
+
|
70
|
+
def header_part(key, content_type, filename)
|
71
|
+
header = "--#{@boundary}\r\n".b
|
72
|
+
header << "Content-Disposition: form-data; name=#{key.inspect}".b
|
73
|
+
header << "; filename=#{filename.inspect}" if filename
|
74
|
+
header << "\r\nContent-Type: #{content_type}\r\n\r\n"
|
75
|
+
StringIO.new(header)
|
76
|
+
end
|
77
|
+
|
78
|
+
def read_chunks(buffer, length = nil)
|
79
|
+
while @part_index < @parts.size
|
80
|
+
chunk = read_from_part(length)
|
81
|
+
|
82
|
+
next unless chunk
|
83
|
+
|
84
|
+
buffer << chunk.force_encoding(Encoding::BINARY)
|
85
|
+
|
86
|
+
next unless length
|
87
|
+
|
88
|
+
length -= chunk.bytesize
|
89
|
+
|
90
|
+
break if length.zero?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# if there's a current part to read from, tries to read a chunk.
|
95
|
+
def read_from_part(max_length = nil)
|
96
|
+
part = @parts[@part_index]
|
97
|
+
|
98
|
+
chunk = part.read(max_length, @buffer)
|
99
|
+
|
100
|
+
return chunk if chunk && !chunk.empty?
|
101
|
+
|
102
|
+
part.close if part.respond_to?(:close)
|
103
|
+
|
104
|
+
@part_index += 1
|
105
|
+
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins::Multipart
|
5
|
+
module Part
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(value)
|
9
|
+
# take out specialized objects of the way
|
10
|
+
if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
|
11
|
+
return [value, value.content_type, value.filename]
|
12
|
+
end
|
13
|
+
|
14
|
+
content_type = filename = nil
|
15
|
+
|
16
|
+
if value.is_a?(Hash)
|
17
|
+
content_type = value[:content_type]
|
18
|
+
filename = value[:filename]
|
19
|
+
value = value[:body]
|
20
|
+
end
|
21
|
+
|
22
|
+
value = value.open(:binmode => true) if value.is_a?(Pathname)
|
23
|
+
|
24
|
+
if value.is_a?(File)
|
25
|
+
filename ||= File.basename(value.path)
|
26
|
+
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
|
27
|
+
[value, content_type, filename]
|
28
|
+
else
|
29
|
+
[StringIO.new(value.to_s), "text/plain"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -81,7 +81,7 @@ module HTTPX
|
|
81
81
|
request.uri.to_s
|
82
82
|
end
|
83
83
|
|
84
|
-
def
|
84
|
+
def set_protocol_headers(request)
|
85
85
|
super
|
86
86
|
proxy_params = @options.proxy
|
87
87
|
request.headers["proxy-authorization"] = "Basic #{proxy_params.token_authentication}" if proxy_params.authenticated?
|
@@ -35,6 +35,14 @@ module HTTPX
|
|
35
35
|
super || @state == :authenticating || @state == :negotiating
|
36
36
|
end
|
37
37
|
|
38
|
+
def interests
|
39
|
+
if @state == :connecting || @state == :authenticating || @state == :negotiating
|
40
|
+
return @write_buffer.empty? ? :r : :w
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
38
46
|
private
|
39
47
|
|
40
48
|
def transition(nextstate)
|
@@ -159,9 +167,10 @@ module HTTPX
|
|
159
167
|
packet = [VERSION, CONNECT, 0].pack("C*")
|
160
168
|
begin
|
161
169
|
ip = IPAddr.new(uri.host)
|
162
|
-
raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
|
163
170
|
|
164
|
-
|
171
|
+
ipcode = ip.ipv6? ? IPV6 : IPV4
|
172
|
+
|
173
|
+
packet << [ipcode].pack("C") << ip.hton
|
165
174
|
rescue IPAddr::InvalidAddressError
|
166
175
|
packet << [DOMAIN, uri.host.bytesize, uri.host].pack("CCA*")
|
167
176
|
end
|