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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -5
  3. data/doc/release_notes/0_11_0.md +76 -0
  4. data/doc/release_notes/0_11_1.md +5 -0
  5. data/doc/release_notes/0_11_2.md +5 -0
  6. data/doc/release_notes/0_11_3.md +5 -0
  7. data/doc/release_notes/0_12_0.md +55 -0
  8. data/lib/httpx.rb +2 -1
  9. data/lib/httpx/adapters/datadog.rb +205 -0
  10. data/lib/httpx/adapters/faraday.rb +4 -8
  11. data/lib/httpx/adapters/webmock.rb +123 -0
  12. data/lib/httpx/altsvc.rb +1 -0
  13. data/lib/httpx/chainable.rb +1 -1
  14. data/lib/httpx/connection.rb +63 -15
  15. data/lib/httpx/connection/http1.rb +16 -5
  16. data/lib/httpx/connection/http2.rb +36 -29
  17. data/lib/httpx/domain_name.rb +1 -3
  18. data/lib/httpx/errors.rb +2 -0
  19. data/lib/httpx/headers.rb +1 -0
  20. data/lib/httpx/io.rb +16 -3
  21. data/lib/httpx/io/ssl.rb +7 -13
  22. data/lib/httpx/io/tcp.rb +9 -8
  23. data/lib/httpx/io/tls.rb +218 -0
  24. data/lib/httpx/io/tls/box.rb +365 -0
  25. data/lib/httpx/io/tls/context.rb +199 -0
  26. data/lib/httpx/io/tls/ffi.rb +390 -0
  27. data/lib/httpx/io/udp.rb +4 -3
  28. data/lib/httpx/parser/http1.rb +4 -4
  29. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  30. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  31. data/lib/httpx/plugins/compression.rb +1 -1
  32. data/lib/httpx/plugins/compression/deflate.rb +2 -5
  33. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  34. data/lib/httpx/plugins/expect.rb +33 -8
  35. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  36. data/lib/httpx/plugins/multipart.rb +42 -35
  37. data/lib/httpx/plugins/multipart/encoder.rb +110 -0
  38. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  39. data/lib/httpx/plugins/multipart/part.rb +34 -0
  40. data/lib/httpx/plugins/proxy.rb +1 -1
  41. data/lib/httpx/plugins/proxy/http.rb +1 -1
  42. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  43. data/lib/httpx/plugins/proxy/socks5.rb +11 -2
  44. data/lib/httpx/plugins/push_promise.rb +5 -4
  45. data/lib/httpx/plugins/retries.rb +1 -1
  46. data/lib/httpx/plugins/stream.rb +3 -5
  47. data/lib/httpx/pool.rb +0 -1
  48. data/lib/httpx/registry.rb +1 -7
  49. data/lib/httpx/request.rb +32 -12
  50. data/lib/httpx/resolver.rb +7 -4
  51. data/lib/httpx/resolver/https.rb +7 -13
  52. data/lib/httpx/resolver/native.rb +10 -6
  53. data/lib/httpx/resolver/system.rb +1 -1
  54. data/lib/httpx/response.rb +9 -2
  55. data/lib/httpx/selector.rb +6 -0
  56. data/lib/httpx/session.rb +40 -20
  57. data/lib/httpx/transcoder.rb +6 -4
  58. data/lib/httpx/transcoder/body.rb +3 -5
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/connection/http1.rbs +2 -2
  61. data/sig/connection/http2.rbs +8 -7
  62. data/sig/headers.rbs +3 -0
  63. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  64. data/sig/plugins/aws_sigv4.rbs +65 -0
  65. data/sig/plugins/multipart.rbs +27 -4
  66. data/sig/plugins/push_promise.rbs +1 -1
  67. data/sig/request.rbs +1 -1
  68. data/sig/resolver/https.rbs +2 -0
  69. data/sig/response.rbs +1 -1
  70. data/sig/session.rbs +1 -1
  71. data/sig/transcoder.rbs +2 -2
  72. data/sig/transcoder/body.rbs +2 -0
  73. data/sig/transcoder/form.rbs +7 -1
  74. data/sig/transcoder/json.rbs +3 -1
  75. metadata +50 -47
  76. 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
- module FormTranscoder
14
- module_function
15
-
16
- class Encoder
17
- extend Forwardable
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
- def_delegator :@raw, :read
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
- def initialize(form)
26
- @raw = if multipart?(form)
27
- HTTP::FormData::Multipart.new(Hash[form.flat_map { |k, v| Transcoder.enum_for(:normalize_keys, k, v).to_a }])
28
- else
29
- HTTP::FormData::Urlencoded.new(form, :encoder => Transcoder::Form.method(:encode))
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
- def bytesize
34
- @raw.content_length
35
- end
43
+ def configure(*)
44
+ Transcoder.register("form", FormTranscoder)
45
+ end
46
+ end
36
47
 
37
- private
48
+ module FormTranscoder
49
+ module_function
38
50
 
39
- def multipart?(data)
40
- data.any? do |_, v|
41
- v.is_a?(HTTP::FormData::Part) ||
42
- (v.respond_to?(:to_ary) && v.to_ary.any? { |e| e.is_a?(HTTP::FormData::Part) }) ||
43
- (v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| e.is_a?(HTTP::FormData::Part) })
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 encode(form)
49
- Encoder.new(form)
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
@@ -242,7 +242,7 @@ module HTTPX
242
242
  register_plugin :proxy, Proxy
243
243
  end
244
244
 
245
- class ProxySSL < SSL
245
+ class ProxySSL < IO.registry["ssl"]
246
246
  def initialize(tcp, request_uri, options)
247
247
  @io = tcp.to_io
248
248
  super(request_uri, tcp.addresses, options)
@@ -81,7 +81,7 @@ module HTTPX
81
81
  request.uri.to_s
82
82
  end
83
83
 
84
- def set_request_headers(request)
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?
@@ -16,6 +16,14 @@ module HTTPX
16
16
  Error = Socks4Error
17
17
 
18
18
  module ConnectionMethods
19
+ def interests
20
+ if @state == :connecting
21
+ return @write_buffer.empty? ? :r : :w
22
+ end
23
+
24
+ super
25
+ end
26
+
19
27
  private
20
28
 
21
29
  def transition(nextstate)
@@ -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
- packet << [IPV4, ip.to_i].pack("CN")
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