httpx 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -3
  3. data/doc/release_notes/0_11_0.md +76 -0
  4. data/lib/httpx/adapters/datadog.rb +205 -0
  5. data/lib/httpx/adapters/faraday.rb +0 -2
  6. data/lib/httpx/adapters/webmock.rb +123 -0
  7. data/lib/httpx/chainable.rb +1 -1
  8. data/lib/httpx/connection/http2.rb +4 -4
  9. data/lib/httpx/domain_name.rb +1 -3
  10. data/lib/httpx/errors.rb +2 -0
  11. data/lib/httpx/headers.rb +1 -0
  12. data/lib/httpx/io/ssl.rb +4 -8
  13. data/lib/httpx/io/udp.rb +1 -1
  14. data/lib/httpx/plugins/expect.rb +33 -8
  15. data/lib/httpx/plugins/multipart.rb +40 -35
  16. data/lib/httpx/plugins/multipart/encoder.rb +115 -0
  17. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  18. data/lib/httpx/plugins/multipart/part.rb +34 -0
  19. data/lib/httpx/plugins/proxy/socks5.rb +3 -2
  20. data/lib/httpx/plugins/push_promise.rb +2 -2
  21. data/lib/httpx/request.rb +21 -11
  22. data/lib/httpx/resolver.rb +7 -4
  23. data/lib/httpx/resolver/https.rb +4 -2
  24. data/lib/httpx/resolver/native.rb +10 -6
  25. data/lib/httpx/resolver/system.rb +1 -1
  26. data/lib/httpx/selector.rb +1 -0
  27. data/lib/httpx/session.rb +15 -18
  28. data/lib/httpx/transcoder.rb +6 -4
  29. data/lib/httpx/version.rb +1 -1
  30. data/sig/connection/http2.rbs +3 -4
  31. data/sig/headers.rbs +3 -0
  32. data/sig/plugins/multipart.rbs +27 -4
  33. data/sig/request.rbs +1 -1
  34. data/sig/resolver/https.rbs +2 -0
  35. data/sig/response.rbs +1 -1
  36. data/sig/session.rbs +1 -1
  37. data/sig/transcoder.rbs +2 -2
  38. data/sig/transcoder/body.rbs +2 -0
  39. data/sig/transcoder/form.rbs +7 -1
  40. data/sig/transcoder/json.rbs +3 -1
  41. metadata +9 -23
  42. data/sig/missing.rbs +0 -12
@@ -59,7 +59,7 @@ module HTTPX
59
59
  private
60
60
 
61
61
  def default_options
62
- @options || Options.new
62
+ @options || Session.default_options
63
63
  end
64
64
 
65
65
  def branch(options, &blk)
@@ -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)
@@ -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
@@ -24,6 +24,8 @@ module HTTPX
24
24
 
25
25
  ConnectTimeoutError = Class.new(TimeoutError)
26
26
 
27
+ ResolveTimeoutError = Class.new(TimeoutError)
28
+
27
29
  ResolveError = Class.new(Error)
28
30
 
29
31
  NativeResolveError = Class.new(ResolveError) do
@@ -119,6 +119,7 @@ module HTTPX
119
119
  def to_hash
120
120
  Hash[to_a]
121
121
  end
122
+ alias_method :to_h, :to_hash
122
123
 
123
124
  # the headers store in array of pairs format
124
125
  def to_a
@@ -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 = @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(@hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
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
 
@@ -25,7 +25,7 @@ module HTTPX
25
25
  true
26
26
  end
27
27
 
28
- if RUBY_VERSION < "2.2"
28
+ if RUBY_VERSION < "2.3"
29
29
  # :nocov:
30
30
  def close
31
31
  @io.close
@@ -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 RequestBodyMethods
32
- def initialize(*, options)
35
+ module RequestMethods
36
+ def initialize(*)
33
37
  super
34
- return if @body.nil?
38
+ return if @body.empty?
35
39
 
36
- threshold = options.expect_threshold_size
37
- return if threshold && !unbounded_body? && @body.bytesize < 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(:expects) do
69
+ request.once(:expect) do
46
70
  @timers.after(@options.expect_timeout) do
47
- if request.state == :expects && !request.expects?
71
+ if request.state == :expect && !request.expects?
72
+ Expect.no_expect_store << request.origin
48
73
  request.headers.delete("expect")
49
- handle(request)
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
- 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
+ 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
- def bytesize
34
- @raw.content_length
35
- end
41
+ def configure(*)
42
+ Transcoder.register("form", FormTranscoder)
43
+ end
44
+ end
36
45
 
37
- private
46
+ module FormTranscoder
47
+ module_function
38
48
 
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
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 encode(form)
49
- Encoder.new(form)
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