httpx 0.10.1 → 0.11.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -3
  3. data/doc/release_notes/0_10_1.md +1 -3
  4. data/doc/release_notes/0_10_2.md +5 -0
  5. data/doc/release_notes/0_11_0.md +76 -0
  6. data/doc/release_notes/0_11_1.md +1 -0
  7. data/doc/release_notes/0_11_2.md +5 -0
  8. data/doc/release_notes/0_11_3.md +5 -0
  9. data/lib/httpx/adapters/datadog.rb +205 -0
  10. data/lib/httpx/adapters/faraday.rb +0 -2
  11. data/lib/httpx/adapters/webmock.rb +123 -0
  12. data/lib/httpx/chainable.rb +1 -1
  13. data/lib/httpx/connection/http1.rb +10 -0
  14. data/lib/httpx/connection/http2.rb +4 -4
  15. data/lib/httpx/domain_name.rb +1 -3
  16. data/lib/httpx/errors.rb +2 -0
  17. data/lib/httpx/headers.rb +1 -0
  18. data/lib/httpx/io/ssl.rb +4 -8
  19. data/lib/httpx/io/udp.rb +4 -3
  20. data/lib/httpx/plugins/compression.rb +1 -1
  21. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  22. data/lib/httpx/plugins/expect.rb +33 -8
  23. data/lib/httpx/plugins/multipart.rb +40 -35
  24. data/lib/httpx/plugins/multipart/encoder.rb +115 -0
  25. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  26. data/lib/httpx/plugins/multipart/part.rb +34 -0
  27. data/lib/httpx/plugins/proxy/socks5.rb +3 -2
  28. data/lib/httpx/plugins/push_promise.rb +2 -2
  29. data/lib/httpx/request.rb +21 -11
  30. data/lib/httpx/resolver.rb +7 -4
  31. data/lib/httpx/resolver/https.rb +4 -2
  32. data/lib/httpx/resolver/native.rb +10 -6
  33. data/lib/httpx/resolver/system.rb +1 -1
  34. data/lib/httpx/selector.rb +1 -0
  35. data/lib/httpx/session.rb +15 -18
  36. data/lib/httpx/transcoder.rb +6 -4
  37. data/lib/httpx/version.rb +1 -1
  38. data/sig/connection/http2.rbs +3 -4
  39. data/sig/headers.rbs +3 -0
  40. data/sig/plugins/multipart.rbs +27 -4
  41. data/sig/request.rbs +1 -1
  42. data/sig/resolver/https.rbs +2 -0
  43. data/sig/response.rbs +1 -1
  44. data/sig/session.rbs +1 -1
  45. data/sig/transcoder.rbs +2 -2
  46. data/sig/transcoder/body.rbs +2 -0
  47. data/sig/transcoder/form.rbs +7 -1
  48. data/sig/transcoder/json.rbs +3 -1
  49. metadata +40 -46
  50. data/sig/missing.rbs +0 -12
@@ -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
@@ -159,9 +159,10 @@ module HTTPX
159
159
  packet = [VERSION, CONNECT, 0].pack("C*")
160
160
  begin
161
161
  ip = IPAddr.new(uri.host)
162
- raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
163
162
 
164
- packet << [IPV4, ip.to_i].pack("CN")
163
+ ipcode = ip.ipv6? ? IPV6 : IPV4
164
+
165
+ packet << [ipcode].pack("C") << ip.hton
165
166
  rescue IPAddr::InvalidAddressError
166
167
  packet << [DOMAIN, uri.host.bytesize, uri.host].pack("CCA*")
167
168
  end
@@ -70,8 +70,8 @@ module HTTPX
70
70
  request.transition(:done)
71
71
  response = request.response
72
72
  response.mark_as_pushed!
73
- stream.on(:data, &parser.method(:on_stream_data).curry[stream, request])
74
- stream.on(:close, &parser.method(:on_stream_close).curry[stream, request])
73
+ stream.on(:data, &parser.method(:on_stream_data).curry(3)[stream, request])
74
+ stream.on(:close, &parser.method(:on_stream_close).curry(3)[stream, request])
75
75
  end
76
76
  end
77
77
  end
data/lib/httpx/request.rb CHANGED
@@ -81,6 +81,10 @@ module HTTPX
81
81
  def response=(response)
82
82
  return unless response
83
83
 
84
+ if response.status == 100
85
+ @informational_status = response.status
86
+ return
87
+ end
84
88
  @response = response
85
89
  end
86
90
 
@@ -170,6 +174,12 @@ module HTTPX
170
174
  end
171
175
  end
172
176
 
177
+ def rewind
178
+ return if empty?
179
+
180
+ @body.rewind if @body.respond_to?(:rewind)
181
+ end
182
+
173
183
  def empty?
174
184
  return true if @body.nil?
175
185
  return false if chunked?
@@ -212,6 +222,7 @@ module HTTPX
212
222
  def transition(nextstate)
213
223
  case nextstate
214
224
  when :idle
225
+ @body.rewind
215
226
  @response = nil
216
227
  @drainer = nil
217
228
  when :headers
@@ -221,15 +232,15 @@ module HTTPX
221
232
  @state == :expect
222
233
 
223
234
  if @headers.key?("expect")
224
- unless @response
225
- @state = :expect
226
- return
227
- end
228
-
229
- case @response.status
230
- when 100
231
- # deallocate
232
- @response = nil
235
+ if @informational_status && @informational_status == 100
236
+ # check for 100 Continue response, and deallocate the var
237
+ # if @informational_status == 100
238
+ # @response = nil
239
+ # end
240
+ else
241
+ return if @state == :expect # do not re-set it
242
+
243
+ nextstate = :expect
233
244
  end
234
245
  end
235
246
  when :done
@@ -241,8 +252,7 @@ module HTTPX
241
252
  end
242
253
 
243
254
  def expects?
244
- @headers["expect"] == "100-continue" &&
245
- @response && @response.status == 100
255
+ @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
246
256
  end
247
257
 
248
258
  class ProcIO
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "resolv"
4
- require "httpx/resolver/resolver_mixin"
5
- require "httpx/resolver/system"
6
- require "httpx/resolver/native"
7
- require "httpx/resolver/https"
8
4
 
9
5
  module HTTPX
10
6
  module Resolver
11
7
  extend Registry
12
8
 
9
+ RESOLVE_TIMEOUT = 5
10
+
11
+ require "httpx/resolver/resolver_mixin"
12
+ require "httpx/resolver/system"
13
+ require "httpx/resolver/native"
14
+ require "httpx/resolver/https"
15
+
13
16
  register :system, System
14
17
  register :native, Native
15
18
  register :https, HTTPS
@@ -37,12 +37,14 @@ module HTTPX
37
37
  @connections = []
38
38
  @uri = URI(@resolver_options[:uri])
39
39
  @uri_addresses = nil
40
+ @resolver = Resolv::DNS.new
41
+ @resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
40
42
  end
41
43
 
42
44
  def <<(connection)
43
45
  return if @uri.origin == connection.origin.to_s
44
46
 
45
- @uri_addresses ||= Resolv.getaddresses(@uri.host)
47
+ @uri_addresses ||= ip_resolve(@uri.host) || system_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
46
48
 
47
49
  if @uri_addresses.empty?
48
50
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
@@ -99,7 +101,7 @@ module HTTPX
99
101
  log { "resolver: query #{type} for #{hostname}" }
100
102
  begin
101
103
  request = build_request(hostname, type)
102
- request.on(:response, &method(:on_response).curry[request])
104
+ request.on(:response, &method(:on_response).curry(2)[request])
103
105
  request.on(:promise, &method(:on_promise))
104
106
  @requests[request] = connection
105
107
  resolver_connection.send(request)
@@ -9,7 +9,6 @@ module HTTPX
9
9
  include Resolver::ResolverMixin
10
10
  using URIExtensions
11
11
 
12
- RESOLVE_TIMEOUT = 5
13
12
  RECORD_TYPES = {
14
13
  "A" => Resolv::DNS::Resource::IN::A,
15
14
  "AAAA" => Resolv::DNS::Resource::IN::AAAA,
@@ -19,7 +18,7 @@ module HTTPX
19
18
  {
20
19
  **Resolv::DNS::Config.default_config_hash,
21
20
  packet_size: 512,
22
- timeouts: RESOLVE_TIMEOUT,
21
+ timeouts: Resolver::RESOLVE_TIMEOUT,
23
22
  record_types: RECORD_TYPES.keys,
24
23
  }.freeze
25
24
  else
@@ -27,7 +26,7 @@ module HTTPX
27
26
  nameserver: nil,
28
27
  **Resolv::DNS::Config.default_config_hash,
29
28
  packet_size: 512,
30
- timeouts: RESOLVE_TIMEOUT,
29
+ timeouts: Resolver::RESOLVE_TIMEOUT,
31
30
  record_types: RECORD_TYPES.keys,
32
31
  }.freeze
33
32
  end
@@ -148,14 +147,19 @@ module HTTPX
148
147
  queries[h] = connection
149
148
  next
150
149
  end
150
+
151
151
  @timeouts[host].shift
152
152
  if @timeouts[host].empty?
153
153
  @timeouts.delete(host)
154
154
  @connections.delete(connection)
155
- raise NativeResolveError.new(connection, host)
155
+ # This loop_time passed to the exception is bogus. Ideally we would pass the total
156
+ # resolve timeout, including from the previous retries.
157
+ raise ResolveTimeoutError.new(loop_time, "Timed out")
158
+ # raise NativeResolveError.new(connection, host)
156
159
  else
160
+ log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
157
161
  connections << connection
158
- log { "resolver: timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..." }
162
+ queries[h] = connection
159
163
  end
160
164
  end
161
165
  @queries = queries
@@ -279,7 +283,7 @@ module HTTPX
279
283
  @io.connect
280
284
  return unless @io.connected?
281
285
 
282
- resolve if @queries.empty?
286
+ resolve if @queries.empty? && !@connections.empty?
283
287
  when :closed
284
288
  return unless @state == :open
285
289
 
@@ -20,7 +20,7 @@ module HTTPX
20
20
  timeouts = resolv_options.delete(:timeouts)
21
21
  resolv_options.delete(:cache)
22
22
  @resolver = Resolv::DNS.new(resolv_options.empty? ? nil : resolv_options)
23
- @resolver.timeouts = timeouts if timeouts
23
+ @resolver.timeouts = timeouts || Resolver::RESOLVE_TIMEOUT
24
24
  end
25
25
 
26
26
  def closed?
@@ -117,6 +117,7 @@ class HTTPX::Selector
117
117
  yield io
118
118
  rescue IOError, SystemCallError
119
119
  @selectables.reject!(&:closed?)
120
+ raise unless @selectables.empty?
120
121
  end
121
122
 
122
123
  def select(interval, &block)
data/lib/httpx/session.rb CHANGED
@@ -41,7 +41,7 @@ module HTTPX
41
41
  def build_request(verb, uri, options = EMPTY_HASH)
42
42
  rklass = @options.request_class
43
43
  request = rklass.new(verb, uri, @options.merge(options).merge(persistent: @persistent))
44
- request.on(:response, &method(:on_response).curry[request])
44
+ request.on(:response, &method(:on_response).curry(2)[request])
45
45
  request.on(:promise, &method(:on_promise))
46
46
  request
47
47
  end
@@ -136,23 +136,20 @@ module HTTPX
136
136
  def build_requests(*args, options)
137
137
  request_options = @options.merge(options)
138
138
 
139
- requests = case args.size
140
- when 1
141
- reqs = args.first
142
- reqs.map do |verb, uri, opts = EMPTY_HASH|
143
- build_request(verb, uri, request_options.merge(opts))
144
- end
145
- when 2
146
- verb, uris = args
147
- if uris.respond_to?(:each)
148
- uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
149
- build_request(verb, uri, request_options.merge(opts))
150
- end
151
- else
152
- [build_request(verb, uris, request_options)]
153
- end
154
- else
155
- raise ArgumentError, "unsupported number of arguments"
139
+ requests = if args.size == 1
140
+ reqs = args.first
141
+ reqs.map do |verb, uri, opts = EMPTY_HASH|
142
+ build_request(verb, uri, request_options.merge(opts))
143
+ end
144
+ else
145
+ verb, uris = args
146
+ if uris.respond_to?(:each)
147
+ uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
148
+ build_request(verb, uri, request_options.merge(opts))
149
+ end
150
+ else
151
+ [build_request(verb, uris, request_options)]
152
+ end
156
153
  end
157
154
  raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty?
158
155
 
@@ -4,18 +4,20 @@ module HTTPX
4
4
  module Transcoder
5
5
  extend Registry
6
6
 
7
- def self.normalize_keys(key, value, &block)
8
- if value.respond_to?(:to_ary)
7
+ def self.normalize_keys(key, value, cond = nil, &block)
8
+ if (cond && cond.call(value))
9
+ block.call(key.to_s, value)
10
+ elsif value.respond_to?(:to_ary)
9
11
  if value.empty?
10
12
  block.call("#{key}[]")
11
13
  else
12
14
  value.to_ary.each do |element|
13
- normalize_keys("#{key}[]", element, &block)
15
+ normalize_keys("#{key}[]", element, cond, &block)
14
16
  end
15
17
  end
16
18
  elsif value.respond_to?(:to_hash)
17
19
  value.to_hash.each do |child_key, child_value|
18
- normalize_keys("#{key}[#{child_key}]", child_value, &block)
20
+ normalize_keys("#{key}[#{child_key}]", child_value, cond, &block)
19
21
  end
20
22
  else
21
23
  block.call(key.to_s, value)
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.3"
5
5
  end
@@ -53,12 +53,11 @@ module HTTPX
53
53
 
54
54
  def join_body: (HTTP2Next::Stream stream, Request request) -> void
55
55
 
56
+ def on_stream_headers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
56
57
 
57
- # def on_stream_headers: (HTTP2Next::Stream stream, Request request, Array[String, String] headers) -> void
58
+ def on_stream_data: (HTTP2Next::Stream stream, Request request, string data) -> void
58
59
 
59
- # def on_stream_data: (HTTP2Next::Stream stream, Request request, string data) -> void
60
-
61
- # def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
60
+ def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
62
61
 
63
62
  def on_frame: (string bytes) -> void
64
63
 
data/sig/headers.rbs CHANGED
@@ -25,6 +25,9 @@ module HTTPX
25
25
  def same_headers?: (untyped headers) -> bool
26
26
 
27
27
  def to_a: () -> Array[[headers_key, String]]
28
+ def to_hash: () -> Hash[headers_key, String]
29
+ alias to_h to_hash
30
+
28
31
  def inspect: () -> String
29
32
 
30
33
  private
@@ -3,18 +3,41 @@ module HTTPX
3
3
  module Multipart
4
4
  def self.load_dependencies: (singleton(Session)) -> void
5
5
  def self.configure: (*untyped) -> void
6
- def self?.encode: (untyped) -> Encoder
6
+ def self?.encode: (untyped) -> (Encoder | Transcoder::Form::Encoder)
7
+
8
+ type multipart_value = string | Pathname | File | _Reader
9
+
10
+ type record_multipart_value = multipart_value |
11
+ { content_type: String, filename: String, body: multipart_value } |
12
+ { content_type: String, body: multipart_value }
13
+
14
+ type multipart_nested_value = multipart_value | _ToAry[multipart_value] | _ToHash[string, multipart_value]
15
+
16
+ type multipart_input = Enumerable[[string, multipart_value], untyped]
7
17
 
8
18
  class Encoder
9
19
  include Transcoder::_Encoder
10
- include _ToS
11
20
  include _Reader
12
21
 
22
+ def content_type: () -> String
23
+
13
24
  private
14
25
 
15
- def initialize: (Hash[Symbol | string, HTTP::FormData::Part | string] multipart_data) -> untyped
26
+ def initialize: (Hash[Symbol | string, multipart_nested_value] multipart_data) -> untyped
27
+
28
+ def header_part: (string key, String content_type, String? filename) -> StringIO
29
+
30
+ def read_chunks: (String buffer, Integer? length) -> void
31
+
32
+ def read_from_part: (Integer? max_length) -> void
33
+ end
34
+
35
+ module Part
36
+ def self?.call: (multipart_nested_value) -> ([_Reader, String, String?] | [_Reader, String])
37
+ end
16
38
 
17
- def multipart?: (top) -> bool
39
+ module MimeTypeDetector
40
+ def self?.call: (IO file, ?String filename) -> String?
18
41
  end
19
42
  end
20
43
  end