httpx 0.10.2 → 0.12.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 +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
|