httpx 0.16.1 → 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_17_0.md +49 -0
  4. data/doc/release_notes/0_18_0.md +69 -0
  5. data/doc/release_notes/0_18_1.md +12 -0
  6. data/doc/release_notes/0_18_2.md +10 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +9 -3
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +4 -4
  12. data/lib/httpx/connection/http1.rb +23 -14
  13. data/lib/httpx/connection/http2.rb +35 -17
  14. data/lib/httpx/connection.rb +74 -76
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/headers.rb +1 -1
  18. data/lib/httpx/io/ssl.rb +5 -1
  19. data/lib/httpx/io/tls.rb +7 -7
  20. data/lib/httpx/loggable.rb +5 -5
  21. data/lib/httpx/options.rb +35 -13
  22. data/lib/httpx/parser/http1.rb +10 -6
  23. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  24. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  25. data/lib/httpx/plugins/compression.rb +5 -3
  26. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +7 -3
  29. data/lib/httpx/plugins/grpc/message.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +3 -3
  31. data/lib/httpx/plugins/h2c.rb +7 -3
  32. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  33. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  34. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  35. data/lib/httpx/plugins/multipart/part.rb +2 -2
  36. data/lib/httpx/plugins/multipart.rb +16 -2
  37. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  38. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  39. data/lib/httpx/plugins/proxy.rb +6 -4
  40. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  41. data/lib/httpx/plugins/response_cache.rb +88 -0
  42. data/lib/httpx/plugins/retries.rb +36 -14
  43. data/lib/httpx/plugins/stream.rb +3 -4
  44. data/lib/httpx/pool.rb +39 -13
  45. data/lib/httpx/registry.rb +1 -1
  46. data/lib/httpx/request.rb +12 -13
  47. data/lib/httpx/resolver/https.rb +5 -7
  48. data/lib/httpx/resolver/native.rb +4 -2
  49. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  50. data/lib/httpx/resolver/system.rb +2 -0
  51. data/lib/httpx/resolver.rb +2 -2
  52. data/lib/httpx/response.rb +60 -44
  53. data/lib/httpx/selector.rb +16 -19
  54. data/lib/httpx/session.rb +22 -15
  55. data/lib/httpx/session2.rb +1 -1
  56. data/lib/httpx/timers.rb +84 -0
  57. data/lib/httpx/transcoder/body.rb +2 -1
  58. data/lib/httpx/transcoder/form.rb +20 -0
  59. data/lib/httpx/transcoder/json.rb +12 -0
  60. data/lib/httpx/transcoder.rb +62 -1
  61. data/lib/httpx/utils.rb +10 -2
  62. data/lib/httpx/version.rb +1 -1
  63. data/lib/httpx.rb +1 -0
  64. data/sig/buffer.rbs +2 -2
  65. data/sig/chainable.rbs +7 -1
  66. data/sig/connection/http1.rbs +15 -4
  67. data/sig/connection/http2.rbs +19 -5
  68. data/sig/connection.rbs +15 -9
  69. data/sig/headers.rbs +19 -18
  70. data/sig/options.rbs +13 -5
  71. data/sig/parser/http1.rbs +3 -3
  72. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  73. data/sig/plugins/aws_sigv4.rbs +12 -3
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/multipart.rbs +64 -8
  76. data/sig/plugins/proxy.rbs +6 -6
  77. data/sig/plugins/response_cache.rbs +35 -0
  78. data/sig/plugins/retries.rbs +3 -0
  79. data/sig/pool.rbs +6 -0
  80. data/sig/request.rbs +11 -8
  81. data/sig/resolver/native.rbs +2 -1
  82. data/sig/resolver/resolver_mixin.rbs +1 -1
  83. data/sig/resolver/system.rbs +3 -1
  84. data/sig/response.rbs +11 -4
  85. data/sig/selector.rbs +8 -6
  86. data/sig/session.rbs +8 -14
  87. data/sig/timers.rbs +32 -0
  88. data/sig/transcoder/form.rbs +1 -0
  89. data/sig/transcoder/json.rbs +1 -0
  90. data/sig/transcoder.rbs +5 -4
  91. data/sig/utils.rbs +4 -0
  92. metadata +18 -17
@@ -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?
@@ -55,21 +57,50 @@ module HTTPX
55
57
  # :nocov:
56
58
  def inspect
57
59
  "#<Response:#{object_id} "\
58
- "HTTP/#{version} " \
59
- "@status=#{@status} " \
60
- "@headers=#{@headers} " \
61
- "@body=#{@body.bytesize}>"
60
+ "HTTP/#{version} " \
61
+ "@status=#{@status} " \
62
+ "@headers=#{@headers} " \
63
+ "@body=#{@body.bytesize}>"
62
64
  end
63
65
  # :nocov:
64
66
 
65
- def raise_for_status
67
+ def error
66
68
  return if @status < 400
67
69
 
68
- raise HTTPError, self
70
+ HTTPError.new(self)
71
+ end
72
+
73
+ def raise_for_status
74
+ return self unless (err = error)
75
+
76
+ raise err
77
+ end
78
+
79
+ def json(options = nil)
80
+ decode("json", options)
81
+ end
82
+
83
+ def form
84
+ decode("form")
69
85
  end
70
86
 
71
87
  private
72
88
 
89
+ def decode(format, options = nil)
90
+ # TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
91
+ transcoder = Transcoder.registry(format)
92
+
93
+ raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decode)
94
+
95
+ decoder = transcoder.decode(self)
96
+
97
+ raise Error, "no decoder available for \"#{format}\"" unless decoder
98
+
99
+ decoder.call(self, options)
100
+ rescue Registry::Error
101
+ raise Error, "no decoder available for \"#{format}\""
102
+ end
103
+
73
104
  def no_data?
74
105
  @status < 200 ||
75
106
  @status == 204 ||
@@ -93,16 +124,6 @@ module HTTPX
93
124
  @length = 0
94
125
  @buffer = nil
95
126
  @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
127
  end
107
128
 
108
129
  def closed?
@@ -196,18 +217,20 @@ module HTTPX
196
217
  end
197
218
 
198
219
  def ==(other)
199
- if other.respond_to?(:read)
200
- _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
201
- else
202
- to_s == other.to_s
220
+ object_id == other.object_id || begin
221
+ if other.respond_to?(:read)
222
+ _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
223
+ else
224
+ to_s == other.to_s
225
+ end
203
226
  end
204
227
  end
205
228
 
206
229
  # :nocov:
207
230
  def inspect
208
231
  "#<HTTPX::Response::Body:#{object_id} " \
209
- "@state=#{@state} " \
210
- "@length=#{@length}>"
232
+ "@state=#{@state} " \
233
+ "@length=#{@length}>"
211
234
  end
212
235
  # :nocov:
213
236
 
@@ -264,30 +287,22 @@ module HTTPX
264
287
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
265
288
  CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
266
289
 
267
- attr_reader :mime_type, :charset
268
-
269
- def initialize(mime_type, charset)
270
- @mime_type = mime_type
271
- @charset = charset
290
+ def initialize(header_value)
291
+ @header_value = header_value
272
292
  end
273
293
 
274
- class << self
275
- # Parse string and return ContentType struct
276
- def parse(str)
277
- new(mime_type(str), charset(str))
278
- end
294
+ def mime_type
295
+ return @mime_type if defined?(@mime_type)
279
296
 
280
- private
297
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
298
+ m && @mime_type = m.strip.downcase
299
+ end
281
300
 
282
- def mime_type(str)
283
- m = str.to_s[MIME_TYPE_RE, 1]
284
- m && m.strip.downcase
285
- end
301
+ def charset
302
+ return @charset if defined?(@charset)
286
303
 
287
- def charset(str)
288
- m = str.to_s[CHARSET_RE, 1]
289
- m && m.strip.delete('"')
290
- end
304
+ m = @header_value.to_s[CHARSET_RE, 1]
305
+ m && @charset = m.strip.delete('"')
291
306
  end
292
307
  end
293
308
 
@@ -304,17 +319,18 @@ module HTTPX
304
319
  end
305
320
 
306
321
  def status
322
+ warn ":#{__method__} is deprecated, use :error.message instead"
307
323
  @error.message
308
324
  end
309
325
 
310
326
  if Exception.method_defined?(:full_message)
311
327
  def to_s
312
- @error.full_message
328
+ @error.full_message(highlight: false)
313
329
  end
314
330
  else
315
331
  def to_s
316
332
  "#{@error.message} (#{@error.class})\n" \
317
- "#{@error.backtrace.join("\n") if @error.backtrace}"
333
+ "#{@error.backtrace.join("\n") if @error.backtrace}"
318
334
  end
319
335
  end
320
336
 
@@ -2,20 +2,6 @@
2
2
 
3
3
  require "io/wait"
4
4
 
5
- module IOExtensions
6
- refine IO do
7
- # provides a fallback for rubies where IO#wait isn't implemented,
8
- # but IO#wait_readable and IO#wait_writable are.
9
- def wait(timeout = nil, _mode = :read_write)
10
- r, w = IO.select([self], [self], nil, timeout)
11
-
12
- return unless r || w
13
-
14
- self
15
- end
16
- end
17
- end
18
-
19
5
  class HTTPX::Selector
20
6
  READABLE = %i[rw r].freeze
21
7
  WRITABLE = %i[rw w].freeze
@@ -23,7 +9,7 @@ class HTTPX::Selector
23
9
  private_constant :READABLE
24
10
  private_constant :WRITABLE
25
11
 
26
- using IOExtensions unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
12
+ using HTTPX::IOExtensions
27
13
 
28
14
  def initialize
29
15
  @selectables = []
@@ -58,11 +44,13 @@ class HTTPX::Selector
58
44
  selectables = @selectables
59
45
  @selectables = []
60
46
 
61
- selectables.each do |io|
47
+ selectables.delete_if do |io|
62
48
  interests = io.interests
63
49
 
64
50
  (r ||= []) << io if READABLE.include?(interests)
65
51
  (w ||= []) << io if WRITABLE.include?(interests)
52
+
53
+ io.state == :closed
66
54
  end
67
55
 
68
56
  if @selectables.empty?
@@ -70,7 +58,7 @@ class HTTPX::Selector
70
58
 
71
59
  # do not run event loop if there's nothing to wait on.
72
60
  # this might happen if connect failed and connection was unregistered.
73
- return if (!r || r.empty?) && (!w || w.empty?)
61
+ return if (!r || r.empty?) && (!w || w.empty?) && !selectables.empty?
74
62
 
75
63
  break
76
64
  else
@@ -86,7 +74,7 @@ class HTTPX::Selector
86
74
 
87
75
  readers, writers = IO.select(r, w, nil, interval)
88
76
 
89
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
77
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil? && interval
90
78
  rescue IOError, SystemCallError
91
79
  @selectables.reject!(&:closed?)
92
80
  retry
@@ -109,6 +97,8 @@ class HTTPX::Selector
109
97
  def select_one(interval)
110
98
  io = @selectables.first
111
99
 
100
+ return unless io
101
+
112
102
  interests = io.interests
113
103
 
114
104
  result = case interests
@@ -118,7 +108,7 @@ class HTTPX::Selector
118
108
  when nil then return
119
109
  end
120
110
 
121
- raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result
111
+ raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") unless result || interval.nil?
122
112
 
123
113
  yield io
124
114
  rescue IOError, SystemCallError
@@ -127,6 +117,13 @@ class HTTPX::Selector
127
117
  end
128
118
 
129
119
  def select(interval, &block)
120
+ # do not cause an infinite loop here.
121
+ #
122
+ # this may happen if timeout calculation actually triggered an error which causes
123
+ # the connections to be reaped (such as the total timeout error) before #select
124
+ # gets called.
125
+ return if interval.nil? && @selectables.empty?
126
+
130
127
  return select_one(interval, &block) if @selectables.size == 1
131
128
 
132
129
  select_many(interval, &block)
data/lib/httpx/session.rb CHANGED
@@ -11,7 +11,7 @@ module HTTPX
11
11
  @options = self.class.default_options.merge(options)
12
12
  @responses = {}
13
13
  @persistent = @options.persistent
14
- wrap(&blk) if block_given?
14
+ wrap(&blk) if blk
15
15
  end
16
16
 
17
17
  def wrap
@@ -21,6 +21,7 @@ module HTTPX
21
21
  yield self
22
22
  ensure
23
23
  @persistent = prev_persistent
24
+ close unless @persistent
24
25
  end
25
26
  end
26
27
 
@@ -32,7 +33,7 @@ module HTTPX
32
33
  raise ArgumentError, "must perform at least one request" if args.empty?
33
34
 
34
35
  requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
35
- responses = send_requests(*requests, options)
36
+ responses = send_requests(*requests)
36
37
  return responses.first if responses.size == 1
37
38
 
38
39
  responses
@@ -40,7 +41,8 @@ module HTTPX
40
41
 
41
42
  def build_request(verb, uri, options = EMPTY_HASH)
42
43
  rklass = @options.request_class
43
- request = rklass.new(verb, uri, @options.merge(options).merge(persistent: @persistent))
44
+ options = @options.merge(options) unless options.is_a?(Options)
45
+ request = rklass.new(verb, uri, options.merge(persistent: @persistent))
44
46
  request.on(:response, &method(:on_response).curry(2)[request])
45
47
  request.on(:promise, &method(:on_promise))
46
48
  request
@@ -174,37 +176,38 @@ module HTTPX
174
176
  end
175
177
  end
176
178
 
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)
179
+ def send_requests(*requests)
180
+ connections = _send_requests(requests)
181
+ receive_requests(requests, connections)
182
182
  end
183
183
 
184
- def _send_requests(requests, options)
184
+ def _send_requests(requests)
185
185
  connections = []
186
186
 
187
187
  requests.each do |request|
188
188
  error = catch(:resolve_error) do
189
- connection = find_connection(request, connections, options)
189
+ connection = find_connection(request, connections, request.options)
190
190
  connection.send(request)
191
191
  end
192
192
  next unless error.is_a?(ResolveError)
193
193
 
194
- request.emit(:response, ErrorResponse.new(request, error, options))
194
+ request.emit(:response, ErrorResponse.new(request, error, request.options))
195
195
  end
196
196
 
197
197
  connections
198
198
  end
199
199
 
200
- def receive_requests(requests, connections, options)
200
+ def receive_requests(requests, connections)
201
201
  responses = []
202
202
 
203
203
  begin
204
204
  # guarantee ordered responses
205
205
  loop do
206
206
  request = requests.first
207
- pool.next_tick until (response = fetch_response(request, connections, options))
207
+
208
+ return responses unless request
209
+
210
+ pool.next_tick until (response = fetch_response(request, connections, request.options))
208
211
 
209
212
  responses << response
210
213
  requests.shift
@@ -218,13 +221,17 @@ module HTTPX
218
221
  # opportunity to traverse the requests, hence we're returning only a fraction of the errors
219
222
  # we were supposed to. This effectively fetches the existing responses and return them.
220
223
  while (request = requests.shift)
221
- responses << fetch_response(request, connections, options)
224
+ responses << fetch_response(request, connections, request.options)
222
225
  end
223
226
  break
224
227
  end
225
228
  responses
226
229
  ensure
227
- close(connections) unless @persistent
230
+ if @persistent
231
+ pool.deactivate(connections)
232
+ else
233
+ close(connections)
234
+ end
228
235
  end
229
236
  end
230
237
 
@@ -7,7 +7,7 @@ module HTTPX
7
7
  @options = self.class.default_options.merge(options)
8
8
  @responses = {}
9
9
  @persistent = @options.persistent
10
- wrap(&blk) if block_given?
10
+ wrap(&blk) if blk
11
11
  end
12
12
 
13
13
  def wrap
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class Timers
5
+ def initialize
6
+ @intervals = []
7
+ end
8
+
9
+ def after(interval_in_secs, &blk)
10
+ return unless interval_in_secs
11
+
12
+ # I'm assuming here that most requests will have the same
13
+ # request timeout, as in most cases they share common set of
14
+ # options. A user setting different request timeouts for 100s of
15
+ # requests will already have a hard time dealing with that.
16
+ unless (interval = @intervals.find { |t| t == interval_in_secs })
17
+ interval = Interval.new(interval_in_secs)
18
+ @intervals << interval
19
+ @intervals.sort!
20
+ end
21
+
22
+ interval << blk
23
+ end
24
+
25
+ def wait_interval
26
+ return if @intervals.empty?
27
+
28
+ @next_interval_at = Utils.now
29
+
30
+ @intervals.first.interval
31
+ end
32
+
33
+ def fire(error = nil)
34
+ raise error if error && error.timeout != @intervals.first
35
+ return if @intervals.empty? || !@next_interval_at
36
+
37
+ elapsed_time = Utils.elapsed_time(@next_interval_at)
38
+
39
+ @intervals.delete_if { |interval| interval.elapse(elapsed_time) <= 0 }
40
+ end
41
+
42
+ def cancel
43
+ @intervals.clear
44
+ end
45
+
46
+ class Interval
47
+ include Comparable
48
+
49
+ attr_reader :interval
50
+
51
+ def initialize(interval)
52
+ @interval = interval
53
+ @callbacks = []
54
+ end
55
+
56
+ def <=>(other)
57
+ @interval <=> other.interval
58
+ end
59
+
60
+ def ==(other)
61
+ return @interval == other if other.is_a?(Numeric)
62
+
63
+ @interval == other.to_f # rubocop:disable Lint/FloatComparison
64
+ end
65
+
66
+ def to_f
67
+ @interval
68
+ end
69
+
70
+ def <<(callback)
71
+ @callbacks << callback
72
+ end
73
+
74
+ def elapse(elapsed)
75
+ @interval -= elapsed
76
+
77
+ @callbacks.each(&:call) if @interval <= 0
78
+
79
+ @interval
80
+ end
81
+ end
82
+ private_constant :Interval
83
+ end
84
+ end
@@ -9,6 +9,7 @@ module HTTPX::Transcoder
9
9
  module_function
10
10
 
11
11
  class Encoder
12
+ using HTTPX::ArrayExtensions
12
13
  extend Forwardable
13
14
 
14
15
  def_delegator :@raw, :to_s
@@ -21,7 +22,7 @@ module HTTPX::Transcoder
21
22
  if @raw.respond_to?(:bytesize)
22
23
  @raw.bytesize
23
24
  elsif @raw.respond_to?(:to_ary)
24
- @raw.map(&:bytesize).reduce(0, :+)
25
+ @raw.sum(&:bytesize)
25
26
  elsif @raw.respond_to?(:size)
26
27
  @raw.size || Float::INFINITY
27
28
  elsif @raw.respond_to?(:length)
@@ -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 HTTPX::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 HTTPX::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
@@ -6,6 +6,14 @@ module HTTPX
6
6
 
7
7
  module_function
8
8
 
9
+ def now
10
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+ end
12
+
13
+ def elapsed_time(monotonic_timestamp)
14
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - monotonic_timestamp
15
+ end
16
+
9
17
  # The value of this field can be either an HTTP-date or a number of
10
18
  # seconds to delay after the response is received.
11
19
  def parse_retry_after(retry_after)
@@ -28,9 +36,9 @@ module HTTPX
28
36
  URIParser = URI::RFC2396_Parser.new
29
37
 
30
38
  def to_uri(uri)
31
- return Kernel.URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
39
+ return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
32
40
 
33
- uri = Kernel.URI(URIParser.escape(uri))
41
+ uri = URI(URIParser.escape(uri))
34
42
 
35
43
  non_ascii_hostname = URIParser.unescape(uri.host)
36
44
 
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.18.2"
5
5
  end
data/lib/httpx.rb CHANGED
@@ -13,6 +13,7 @@ require "httpx/callbacks"
13
13
  require "httpx/loggable"
14
14
  require "httpx/registry"
15
15
  require "httpx/transcoder"
16
+ require "httpx/timers"
16
17
  require "httpx/pool"
17
18
  require "httpx/headers"
18
19
  require "httpx/request"
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