httpx 0.16.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_17_0.md +49 -0
  3. data/lib/httpx/adapters/webmock.rb +2 -2
  4. data/lib/httpx/chainable.rb +1 -1
  5. data/lib/httpx/connection/http1.rb +15 -9
  6. data/lib/httpx/connection/http2.rb +13 -10
  7. data/lib/httpx/connection.rb +4 -5
  8. data/lib/httpx/headers.rb +1 -1
  9. data/lib/httpx/options.rb +28 -6
  10. data/lib/httpx/parser/http1.rb +10 -6
  11. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  12. data/lib/httpx/plugins/h2c.rb +7 -3
  13. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  14. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  15. data/lib/httpx/plugins/multipart/part.rb +2 -2
  16. data/lib/httpx/plugins/multipart.rb +14 -0
  17. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  18. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  19. data/lib/httpx/plugins/proxy.rb +6 -4
  20. data/lib/httpx/plugins/stream.rb +2 -3
  21. data/lib/httpx/registry.rb +1 -1
  22. data/lib/httpx/request.rb +6 -7
  23. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  24. data/lib/httpx/response.rb +37 -30
  25. data/lib/httpx/selector.rb +4 -2
  26. data/lib/httpx/session.rb +15 -13
  27. data/lib/httpx/transcoder/form.rb +20 -0
  28. data/lib/httpx/transcoder/json.rb +12 -0
  29. data/lib/httpx/transcoder.rb +62 -1
  30. data/lib/httpx/utils.rb +2 -2
  31. data/lib/httpx/version.rb +1 -1
  32. data/sig/buffer.rbs +2 -2
  33. data/sig/chainable.rbs +6 -1
  34. data/sig/connection/http1.rbs +10 -4
  35. data/sig/connection/http2.rbs +16 -5
  36. data/sig/connection.rbs +4 -4
  37. data/sig/headers.rbs +19 -18
  38. data/sig/options.rbs +13 -5
  39. data/sig/parser/http1.rbs +3 -3
  40. data/sig/plugins/aws_sigv4.rbs +12 -3
  41. data/sig/plugins/basic_authentication.rbs +1 -1
  42. data/sig/plugins/multipart.rbs +64 -8
  43. data/sig/plugins/proxy.rbs +6 -6
  44. data/sig/request.rbs +11 -8
  45. data/sig/resolver/native.rbs +4 -2
  46. data/sig/resolver/resolver_mixin.rbs +1 -1
  47. data/sig/resolver/system.rbs +1 -1
  48. data/sig/response.rbs +8 -2
  49. data/sig/selector.rbs +8 -6
  50. data/sig/session.rbs +8 -14
  51. data/sig/transcoder/form.rbs +1 -0
  52. data/sig/transcoder/json.rbs +1 -0
  53. data/sig/transcoder.rbs +5 -4
  54. metadata +5 -2
@@ -14,6 +14,8 @@ module HTTPX
14
14
 
15
15
  def_delegator :@body, :to_s
16
16
 
17
+ def_delegator :@body, :to_str
18
+
17
19
  def_delegator :@body, :read
18
20
 
19
21
  def_delegator :@body, :copy_to
@@ -45,7 +47,7 @@ module HTTPX
45
47
  end
46
48
 
47
49
  def content_type
48
- ContentType.parse(@headers["content-type"])
50
+ @content_type ||= ContentType.new(@headers["content-type"])
49
51
  end
50
52
 
51
53
  def complete?
@@ -68,8 +70,31 @@ module HTTPX
68
70
  raise HTTPError, self
69
71
  end
70
72
 
73
+ def json(options = nil)
74
+ decode("json", options)
75
+ end
76
+
77
+ def form
78
+ decode("form")
79
+ end
80
+
71
81
  private
72
82
 
83
+ def decode(format, options = nil)
84
+ # TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
85
+ transcoder = Transcoder.registry(format)
86
+
87
+ raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decode)
88
+
89
+ decoder = transcoder.decode(self)
90
+
91
+ raise Error, "no decoder available for \"#{format}\"" unless decoder
92
+
93
+ decoder.call(self, options)
94
+ rescue Registry::Error
95
+ raise Error, "no decoder available for \"#{format}\""
96
+ end
97
+
73
98
  def no_data?
74
99
  @status < 200 ||
75
100
  @status == 204 ||
@@ -93,16 +118,6 @@ module HTTPX
93
118
  @length = 0
94
119
  @buffer = nil
95
120
  @state = :idle
96
- ObjectSpace.define_finalizer(self, self.class.finalize(@buffer))
97
- end
98
-
99
- def self.finalize(buffer)
100
- proc {
101
- return unless buffer
102
-
103
- @buffer.close
104
- @buffer.unlink if @buffer.respond_to?(:unlink)
105
- }
106
121
  end
107
122
 
108
123
  def closed?
@@ -264,30 +279,22 @@ module HTTPX
264
279
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
265
280
  CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
266
281
 
267
- attr_reader :mime_type, :charset
268
-
269
- def initialize(mime_type, charset)
270
- @mime_type = mime_type
271
- @charset = charset
282
+ def initialize(header_value)
283
+ @header_value = header_value
272
284
  end
273
285
 
274
- class << self
275
- # Parse string and return ContentType struct
276
- def parse(str)
277
- new(mime_type(str), charset(str))
278
- end
286
+ def mime_type
287
+ return @mime_type if defined?(@mime_type)
279
288
 
280
- private
289
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
290
+ m && @mime_type = m.strip.downcase
291
+ end
281
292
 
282
- def mime_type(str)
283
- m = str.to_s[MIME_TYPE_RE, 1]
284
- m && m.strip.downcase
285
- end
293
+ def charset
294
+ return @charset if defined?(@charset)
286
295
 
287
- def charset(str)
288
- m = str.to_s[CHARSET_RE, 1]
289
- m && m.strip.delete('"')
290
- end
296
+ m = @header_value.to_s[CHARSET_RE, 1]
297
+ m && @charset = m.strip.delete('"')
291
298
  end
292
299
  end
293
300
 
@@ -86,7 +86,7 @@ class HTTPX::Selector
86
86
 
87
87
  readers, writers = IO.select(r, w, nil, interval)
88
88
 
89
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
89
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil? && interval
90
90
  rescue IOError, SystemCallError
91
91
  @selectables.reject!(&:closed?)
92
92
  retry
@@ -109,6 +109,8 @@ class HTTPX::Selector
109
109
  def select_one(interval)
110
110
  io = @selectables.first
111
111
 
112
+ return unless io
113
+
112
114
  interests = io.interests
113
115
 
114
116
  result = case interests
@@ -118,7 +120,7 @@ class HTTPX::Selector
118
120
  when nil then return
119
121
  end
120
122
 
121
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result
123
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result || interval.nil?
122
124
 
123
125
  yield io
124
126
  rescue IOError, SystemCallError
data/lib/httpx/session.rb CHANGED
@@ -32,7 +32,7 @@ module HTTPX
32
32
  raise ArgumentError, "must perform at least one request" if args.empty?
33
33
 
34
34
  requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
35
- responses = send_requests(*requests, options)
35
+ responses = send_requests(*requests)
36
36
  return responses.first if responses.size == 1
37
37
 
38
38
  responses
@@ -40,7 +40,8 @@ module HTTPX
40
40
 
41
41
  def build_request(verb, uri, options = EMPTY_HASH)
42
42
  rklass = @options.request_class
43
- request = rklass.new(verb, uri, @options.merge(options).merge(persistent: @persistent))
43
+ options = @options.merge(options) unless options.is_a?(Options)
44
+ request = rklass.new(verb, uri, options.merge(persistent: @persistent))
44
45
  request.on(:response, &method(:on_response).curry(2)[request])
45
46
  request.on(:promise, &method(:on_promise))
46
47
  request
@@ -174,37 +175,38 @@ module HTTPX
174
175
  end
175
176
  end
176
177
 
177
- def send_requests(*requests, options)
178
- request_options = @options.merge(options)
179
-
180
- connections = _send_requests(requests, request_options)
181
- receive_requests(requests, connections, request_options)
178
+ def send_requests(*requests)
179
+ connections = _send_requests(requests)
180
+ receive_requests(requests, connections)
182
181
  end
183
182
 
184
- def _send_requests(requests, options)
183
+ def _send_requests(requests)
185
184
  connections = []
186
185
 
187
186
  requests.each do |request|
188
187
  error = catch(:resolve_error) do
189
- connection = find_connection(request, connections, options)
188
+ connection = find_connection(request, connections, request.options)
190
189
  connection.send(request)
191
190
  end
192
191
  next unless error.is_a?(ResolveError)
193
192
 
194
- request.emit(:response, ErrorResponse.new(request, error, options))
193
+ request.emit(:response, ErrorResponse.new(request, error, request.options))
195
194
  end
196
195
 
197
196
  connections
198
197
  end
199
198
 
200
- def receive_requests(requests, connections, options)
199
+ def receive_requests(requests, connections)
201
200
  responses = []
202
201
 
203
202
  begin
204
203
  # guarantee ordered responses
205
204
  loop do
206
205
  request = requests.first
207
- pool.next_tick until (response = fetch_response(request, connections, options))
206
+
207
+ return responses unless request
208
+
209
+ pool.next_tick until (response = fetch_response(request, connections, request.options))
208
210
 
209
211
  responses << response
210
212
  requests.shift
@@ -218,7 +220,7 @@ module HTTPX
218
220
  # opportunity to traverse the requests, hence we're returning only a fraction of the errors
219
221
  # we were supposed to. This effectively fetches the existing responses and return them.
220
222
  while (request = requests.shift)
221
- responses << fetch_response(request, connections, options)
223
+ responses << fetch_response(request, connections, request.options)
222
224
  end
223
225
  break
224
226
  end
@@ -7,6 +7,8 @@ module HTTPX::Transcoder
7
7
  module Form
8
8
  module_function
9
9
 
10
+ PARAM_DEPTH_LIMIT = 32
11
+
10
12
  class Encoder
11
13
  extend Forwardable
12
14
 
@@ -31,9 +33,27 @@ module HTTPX::Transcoder
31
33
  end
32
34
  end
33
35
 
36
+ module Decoder
37
+ module_function
38
+
39
+ def call(response, _)
40
+ URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
41
+ HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
42
+ end
43
+ end
44
+ end
45
+
34
46
  def encode(form)
35
47
  Encoder.new(form)
36
48
  end
49
+
50
+ def decode(response)
51
+ content_type = response.content_type.mime_type
52
+
53
+ raise Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
54
+
55
+ Decoder
56
+ end
37
57
  end
38
58
  register "form", Form
39
59
  end
@@ -5,6 +5,10 @@ require "json"
5
5
 
6
6
  module HTTPX::Transcoder
7
7
  module JSON
8
+ JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
9
+
10
+ using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
11
+
8
12
  module_function
9
13
 
10
14
  class Encoder
@@ -27,6 +31,14 @@ module HTTPX::Transcoder
27
31
  def encode(json)
28
32
  Encoder.new(json)
29
33
  end
34
+
35
+ def decode(response)
36
+ content_type = response.content_type.mime_type
37
+
38
+ raise Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
39
+
40
+ ::JSON.method(:parse)
41
+ end
30
42
  end
31
43
  register "json", JSON
32
44
  end
@@ -4,7 +4,11 @@ module HTTPX
4
4
  module Transcoder
5
5
  extend Registry
6
6
 
7
- def self.normalize_keys(key, value, cond = nil, &block)
7
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
8
+
9
+ module_function
10
+
11
+ def normalize_keys(key, value, cond = nil, &block)
8
12
  if (cond && cond.call(value))
9
13
  block.call(key.to_s, value)
10
14
  elsif value.respond_to?(:to_ary)
@@ -23,6 +27,63 @@ module HTTPX
23
27
  block.call(key.to_s, value)
24
28
  end
25
29
  end
30
+
31
+ # based on https://github.com/rack/rack/blob/d15dd728440710cfc35ed155d66a98dc2c07ae42/lib/rack/query_parser.rb#L82
32
+ def normalize_query(params, name, v, depth)
33
+ raise Error, "params depth surpasses what's supported" if depth <= 0
34
+
35
+ name =~ /\A[\[\]]*([^\[\]]+)\]*/
36
+ k = Regexp.last_match(1) || ""
37
+ after = Regexp.last_match ? Regexp.last_match.post_match : ""
38
+
39
+ if k.empty?
40
+ return Array(v) if !v.empty? && name == "[]"
41
+
42
+ return
43
+ end
44
+
45
+ case after
46
+ when ""
47
+ params[k] = v
48
+ when "["
49
+ params[name] = v
50
+ when "[]"
51
+ params[k] ||= []
52
+ raise Error, "expected Array (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Array)
53
+
54
+ params[k] << v
55
+ when /^\[\]\[([^\[\]]+)\]$/, /^\[\](.+)$/
56
+ child_key = Regexp.last_match(1)
57
+ params[k] ||= []
58
+ raise Error, "expected Array (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Array)
59
+
60
+ if params[k].last.is_a?(Hash) && !params_hash_has_key?(params[k].last, child_key)
61
+ normalize_query(params[k].last, child_key, v, depth - 1)
62
+ else
63
+ params[k] << normalize_query({}, child_key, v, depth - 1)
64
+ end
65
+ else
66
+ params[k] ||= {}
67
+ raise Error, "expected Hash (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Hash)
68
+
69
+ params[k] = normalize_query(params[k], after, v, depth - 1)
70
+ end
71
+
72
+ params
73
+ end
74
+
75
+ def params_hash_has_key?(hash, key)
76
+ return false if /\[\]/.match?(key)
77
+
78
+ key.split(/[\[\]]+/).inject(hash) do |h, part|
79
+ next h if part == ""
80
+ return false unless h.is_a?(Hash) && h.key?(part)
81
+
82
+ h[part]
83
+ end
84
+
85
+ true
86
+ end
26
87
  end
27
88
  end
28
89
 
data/lib/httpx/utils.rb CHANGED
@@ -28,9 +28,9 @@ module HTTPX
28
28
  URIParser = URI::RFC2396_Parser.new
29
29
 
30
30
  def to_uri(uri)
31
- return Kernel.URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
31
+ return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
32
32
 
33
- uri = Kernel.URI(URIParser.escape(uri))
33
+ uri = URI(URIParser.escape(uri))
34
34
 
35
35
  non_ascii_hostname = URIParser.unescape(uri.host)
36
36
 
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.16.1"
4
+ VERSION = "0.17.0"
5
5
  end
data/sig/buffer.rbs CHANGED
@@ -1,7 +1,7 @@
1
1
  module HTTPX
2
2
  class Buffer
3
3
  extend Forwardable
4
-
4
+
5
5
  include _ToS
6
6
  include _ToStr
7
7
 
@@ -13,7 +13,7 @@ module HTTPX
13
13
  def shift!: (Integer) -> void
14
14
 
15
15
  # delegated
16
- def <<: (string data) -> void
16
+ def <<: (string data) -> String
17
17
  def empty?: () -> bool
18
18
  def bytesize: () -> Integer
19
19
  def clear: () -> void
data/sig/chainable.rbs CHANGED
@@ -1,6 +1,11 @@
1
1
  module HTTPX
2
2
  module Chainable
3
- def request: (*untyped, **untyped) -> (response | Array[response])
3
+ def request: (*Request, **untyped) -> Array[response]
4
+ | (Request, **untyped) -> response
5
+ | (verb | string, uri | [uri], **untyped) -> response
6
+ | (Array[[verb | string, uri] | [verb | string, uri, options]], **untyped) -> Array[response]
7
+ | (verb | string, _Each[uri | [uri, options]], **untyped) -> Array[response]
8
+
4
9
  def accept: (String) -> Session
5
10
  def wrap: () { (Session) -> void } -> void
6
11
 
@@ -3,6 +3,10 @@ module HTTPX
3
3
  include Callbacks
4
4
  include Loggable
5
5
 
6
+ UPCASED: Hash[String, String]
7
+ MAX_REQUESTS: Integer
8
+ CRLF: String
9
+
6
10
  attr_reader pending: Array[Request]
7
11
  attr_reader requests: Array[Request]
8
12
 
@@ -30,11 +34,13 @@ module HTTPX
30
34
 
31
35
  def handle_error: (StandardError ex) -> void
32
36
 
37
+ def on_start: () -> void
38
+
33
39
  def on_headers: (Hash[String, Array[String]] headers) -> void
34
40
 
35
41
  def on_trailers: (Hash[String, Array[String]] headers) -> void
36
42
 
37
- def on_data: (string chunk) -> void
43
+ def on_data: (String chunk) -> void
38
44
 
39
45
  def on_complete: () -> void
40
46
 
@@ -42,7 +48,7 @@ module HTTPX
42
48
 
43
49
  def ping: () -> void
44
50
 
45
- def timeout: () -> Integer
51
+ def timeout: () -> Numeric
46
52
 
47
53
  private
48
54
 
@@ -54,7 +60,7 @@ module HTTPX
54
60
 
55
61
  def disable_pipelining: () -> void
56
62
 
57
- def set_protocol_headers: (Request) -> _Each[[headers_key, String]]
63
+ def set_protocol_headers: (Request) -> _Each[[String, String]]
58
64
 
59
65
  def headline_uri: (Request) -> String
60
66
 
@@ -64,7 +70,7 @@ module HTTPX
64
70
 
65
71
  def join_trailers: (Request request) -> void
66
72
 
67
- def join_headers2: (_Each[[headers_key, String]] headers) -> void
73
+ def join_headers2: (_Each[[String, String]] headers) -> void
68
74
 
69
75
  def join_body: (Request request) -> void
70
76