httpx 0.16.1 → 0.17.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 (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