httpx 0.10.1 → 0.11.3

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 (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