httpx 0.17.0 → 0.18.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_18_0.md +69 -0
  4. data/doc/release_notes/0_18_1.md +12 -0
  5. data/doc/release_notes/0_18_2.md +10 -0
  6. data/doc/release_notes/0_18_3.md +7 -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 +7 -1
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +3 -3
  12. data/lib/httpx/connection/http1.rb +8 -5
  13. data/lib/httpx/connection/http2.rb +22 -7
  14. data/lib/httpx/connection.rb +70 -71
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/io/ssl.rb +5 -1
  18. data/lib/httpx/io/tls.rb +7 -7
  19. data/lib/httpx/loggable.rb +5 -5
  20. data/lib/httpx/options.rb +7 -7
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  22. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  23. data/lib/httpx/plugins/compression.rb +5 -3
  24. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  25. data/lib/httpx/plugins/expect.rb +7 -3
  26. data/lib/httpx/plugins/grpc/message.rb +2 -2
  27. data/lib/httpx/plugins/grpc.rb +3 -3
  28. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  29. data/lib/httpx/plugins/multipart.rb +2 -2
  30. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  31. data/lib/httpx/plugins/response_cache.rb +88 -0
  32. data/lib/httpx/plugins/retries.rb +36 -14
  33. data/lib/httpx/plugins/stream.rb +1 -1
  34. data/lib/httpx/pool.rb +39 -13
  35. data/lib/httpx/request.rb +7 -7
  36. data/lib/httpx/resolver/https.rb +5 -7
  37. data/lib/httpx/resolver/native.rb +4 -2
  38. data/lib/httpx/resolver/system.rb +2 -0
  39. data/lib/httpx/resolver.rb +2 -2
  40. data/lib/httpx/response.rb +23 -14
  41. data/lib/httpx/selector.rb +12 -17
  42. data/lib/httpx/session.rb +7 -2
  43. data/lib/httpx/session2.rb +1 -1
  44. data/lib/httpx/timers.rb +84 -0
  45. data/lib/httpx/transcoder/body.rb +2 -1
  46. data/lib/httpx/transcoder/form.rb +1 -1
  47. data/lib/httpx/transcoder/json.rb +1 -1
  48. data/lib/httpx/utils.rb +8 -0
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +1 -0
  51. data/sig/chainable.rbs +1 -0
  52. data/sig/connection/http1.rbs +5 -0
  53. data/sig/connection/http2.rbs +3 -0
  54. data/sig/connection.rbs +12 -6
  55. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  56. data/sig/plugins/response_cache.rbs +35 -0
  57. data/sig/plugins/retries.rbs +3 -0
  58. data/sig/pool.rbs +6 -0
  59. data/sig/resolver/native.rbs +3 -4
  60. data/sig/resolver/system.rbs +2 -0
  61. data/sig/response.rbs +3 -2
  62. data/sig/timers.rbs +32 -0
  63. data/sig/utils.rbs +4 -0
  64. metadata +17 -17
@@ -12,19 +12,29 @@ module HTTPX
12
12
  # TODO: pass max_retries in a configure/load block
13
13
 
14
14
  IDEMPOTENT_METHODS = %i[get options head put delete].freeze
15
- RETRYABLE_ERRORS = [IOError,
16
- EOFError,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- TLSError,
21
- TimeoutError,
22
- Parser::Error,
23
- Errno::EINVAL,
24
- Errno::ETIMEDOUT].freeze
25
-
26
- def self.extra_options(options)
27
- options.merge(max_retries: MAX_RETRIES)
15
+ RETRYABLE_ERRORS = [
16
+ IOError,
17
+ EOFError,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::EINVAL,
22
+ Errno::ETIMEDOUT,
23
+ Parser::Error,
24
+ TLSError,
25
+ TimeoutError,
26
+ Connection::HTTP2::GoawayError,
27
+ ].freeze
28
+ DEFAULT_JITTER = ->(interval) { interval * (0.5 * (1 + rand)) }
29
+
30
+ if ENV.key?("HTTPX_NO_JITTER")
31
+ def self.extra_options(options)
32
+ options.merge(max_retries: MAX_RETRIES)
33
+ end
34
+ else
35
+ def self.extra_options(options)
36
+ options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
37
+ end
28
38
  end
29
39
 
30
40
  module OptionsMethods
@@ -38,6 +48,13 @@ module HTTPX
38
48
  value
39
49
  end
40
50
 
51
+ def option_retry_jitter(value)
52
+ # return early if callable
53
+ raise TypeError, ":retry_jitter must be callable" unless value.respond_to?(:call)
54
+
55
+ value
56
+ end
57
+
41
58
  def option_max_retries(value)
42
59
  num = Integer(value)
43
60
  raise TypeError, ":max_retries must be positive" unless num.positive?
@@ -87,10 +104,15 @@ module HTTPX
87
104
  retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
88
105
 
89
106
  if retry_after
107
+ # apply jitter
108
+ if (jitter = request.options.retry_jitter)
109
+ retry_after = jitter.call(retry_after)
110
+ end
90
111
 
112
+ retry_start = Utils.now
91
113
  log { "retrying after #{retry_after} secs..." }
92
114
  pool.after(retry_after) do
93
- log { "retrying!!" }
115
+ log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
94
116
  connection = find_connection(request, connections, options)
95
117
  connection.send(request)
96
118
  end
@@ -9,7 +9,7 @@ module HTTPX
9
9
  end
10
10
 
11
11
  def each(&block)
12
- return enum_for(__method__) unless block_given?
12
+ return enum_for(__method__) unless block
13
13
 
14
14
  raise Error, "response already streamed" if @response
15
15
 
data/lib/httpx/pool.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "timers"
5
4
  require "httpx/selector"
6
5
  require "httpx/connection"
7
6
  require "httpx/resolver"
8
7
 
9
8
  module HTTPX
10
9
  class Pool
10
+ using ArrayExtensions
11
11
  extend Forwardable
12
12
 
13
13
  def_delegator :@timers, :after
@@ -15,7 +15,7 @@ module HTTPX
15
15
  def initialize
16
16
  @resolvers = {}
17
17
  @_resolver_ios = {}
18
- @timers = Timers::Group.new
18
+ @timers = Timers.new
19
19
  @selector = Selector.new
20
20
  @connections = []
21
21
  @connected_connections = 0
@@ -27,15 +27,18 @@ module HTTPX
27
27
 
28
28
  def next_tick
29
29
  catch(:jump_tick) do
30
- timeout = [next_timeout, @timers.wait_interval].compact.min
30
+ timeout = next_timeout
31
31
  if timeout && timeout.negative?
32
32
  @timers.fire
33
33
  throw(:jump_tick)
34
34
  end
35
35
 
36
- @selector.select(timeout, &:call)
37
-
38
- @timers.fire
36
+ begin
37
+ @selector.select(timeout, &:call)
38
+ @timers.fire
39
+ rescue TimeoutError => e
40
+ @timers.fire(e)
41
+ end
39
42
  end
40
43
  rescue StandardError => e
41
44
  @connections.each do |connection|
@@ -64,6 +67,16 @@ module HTTPX
64
67
  connection.on(:open) do
65
68
  @connected_connections += 1
66
69
  end
70
+ connection.on(:activate) do
71
+ select_connection(connection)
72
+ end
73
+ end
74
+
75
+ def deactivate(connections)
76
+ connections.each do |connection|
77
+ connection.deactivate
78
+ deselect_connection(connection) if connection.state == :inactive
79
+ end
67
80
  end
68
81
 
69
82
  # opens a connection to the IP reachable through +uri+.
@@ -81,7 +94,7 @@ module HTTPX
81
94
  def resolve_connection(connection)
82
95
  @connections << connection unless @connections.include?(connection)
83
96
 
84
- if connection.addresses || connection.state == :open
97
+ if connection.addresses || connection.open?
85
98
  #
86
99
  # there are two cases in which we want to activate initialization of
87
100
  # connection immediately:
@@ -98,7 +111,7 @@ module HTTPX
98
111
  resolver << connection
99
112
  return if resolver.empty?
100
113
 
101
- @_resolver_ios[resolver] ||= @selector.register(resolver)
114
+ @_resolver_ios[resolver] ||= select_connection(resolver)
102
115
  end
103
116
 
104
117
  def on_resolver_connection(connection)
@@ -107,7 +120,7 @@ module HTTPX
107
120
  end
108
121
  return register_connection(connection) unless found_connection
109
122
 
110
- if found_connection.state == :open
123
+ if found_connection.open?
111
124
  coalesce_connections(found_connection, connection)
112
125
  throw(:coalesced, found_connection)
113
126
  else
@@ -129,7 +142,7 @@ module HTTPX
129
142
 
130
143
  @resolvers.delete(resolver_type)
131
144
 
132
- @selector.deregister(resolver)
145
+ deselect_connection(resolver)
133
146
  @_resolver_ios.delete(resolver)
134
147
  resolver.close unless resolver.closed?
135
148
  end
@@ -140,7 +153,7 @@ module HTTPX
140
153
  # consider it connected already.
141
154
  @connected_connections += 1
142
155
  end
143
- @selector.register(connection)
156
+ select_connection(connection)
144
157
  connection.on(:close) do
145
158
  unregister_connection(connection)
146
159
  end
@@ -148,10 +161,18 @@ module HTTPX
148
161
 
149
162
  def unregister_connection(connection)
150
163
  @connections.delete(connection)
151
- @selector.deregister(connection)
164
+ deselect_connection(connection)
152
165
  @connected_connections -= 1
153
166
  end
154
167
 
168
+ def select_connection(connection)
169
+ @selector.register(connection)
170
+ end
171
+
172
+ def deselect_connection(connection)
173
+ @selector.deregister(connection)
174
+ end
175
+
155
176
  def coalesce_connections(conn1, conn2)
156
177
  if conn1.coalescable?(conn2)
157
178
  conn1.merge(conn2)
@@ -162,7 +183,11 @@ module HTTPX
162
183
  end
163
184
 
164
185
  def next_timeout
165
- @resolvers.values.reject(&:closed?).map(&:timeout).compact.min || @connections.map(&:timeout).compact.min
186
+ [
187
+ @timers.wait_interval,
188
+ *@resolvers.values.reject(&:closed?).filter_map(&:timeout),
189
+ *@connections.filter_map(&:timeout),
190
+ ].compact.min
166
191
  end
167
192
 
168
193
  def find_resolver_for(connection)
@@ -172,6 +197,7 @@ module HTTPX
172
197
 
173
198
  @resolvers[resolver_type] ||= begin
174
199
  resolver = resolver_type.new(connection_options)
200
+ resolver.pool = self if resolver.respond_to?(:pool=)
175
201
  resolver.on(:resolve, &method(:on_resolver_connection))
176
202
  resolver.on(:error, &method(:on_resolver_error))
177
203
  resolver.on(:close) { on_resolver_close(resolver) }
data/lib/httpx/request.rb CHANGED
@@ -148,10 +148,10 @@ module HTTPX
148
148
  # :nocov:
149
149
  def inspect
150
150
  "#<HTTPX::Request:#{object_id} " \
151
- "#{@verb.to_s.upcase} " \
152
- "#{uri} " \
153
- "@headers=#{@headers} " \
154
- "@body=#{@body}>"
151
+ "#{@verb.to_s.upcase} " \
152
+ "#{uri} " \
153
+ "@headers=#{@headers} " \
154
+ "@body=#{@body}>"
155
155
  end
156
156
  # :nocov:
157
157
 
@@ -181,7 +181,7 @@ module HTTPX
181
181
  end
182
182
 
183
183
  def each(&block)
184
- return enum_for(__method__) unless block_given?
184
+ return enum_for(__method__) unless block
185
185
  return if @body.nil?
186
186
 
187
187
  body = stream(@body)
@@ -236,7 +236,7 @@ module HTTPX
236
236
  # :nocov:
237
237
  def inspect
238
238
  "#<HTTPX::Request::Body:#{object_id} " \
239
- "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
+ "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
240
240
  end
241
241
  # :nocov:
242
242
  end
@@ -285,7 +285,7 @@ module HTTPX
285
285
  end
286
286
 
287
287
  def write(data)
288
- @block.call(data)
288
+ @block.call(data.dup)
289
289
  data.bytesize
290
290
  end
291
291
  end
@@ -24,7 +24,9 @@ module HTTPX
24
24
  record_types: RECORD_TYPES.keys,
25
25
  }.freeze
26
26
 
27
- def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
27
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
28
+
29
+ attr_writer :pool
28
30
 
29
31
  def initialize(options)
30
32
  @options = Options.new(options)
@@ -63,15 +65,11 @@ module HTTPX
63
65
 
64
66
  private
65
67
 
66
- def pool
67
- Thread.current[:httpx_connection_pool] ||= Pool.new
68
- end
69
-
70
68
  def resolver_connection
71
- @resolver_connection ||= pool.find_connection(@uri, @options) || begin
69
+ @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
72
70
  @building_connection = true
73
71
  connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
74
- pool.init_connection(connection, @options)
72
+ @pool.init_connection(connection, @options)
75
73
  emit_addresses(connection, @uri_addresses)
76
74
  @building_connection = false
77
75
  connection
@@ -47,6 +47,8 @@ module HTTPX
47
47
 
48
48
  def_delegator :@connections, :empty?
49
49
 
50
+ attr_reader :state
51
+
50
52
  def initialize(options)
51
53
  @options = Options.new(options)
52
54
  @ns_index = 0
@@ -120,7 +122,7 @@ module HTTPX
120
122
  def timeout
121
123
  return if @connections.empty?
122
124
 
123
- @start_timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC)
125
+ @start_timeout = Utils.now
124
126
  hosts = @queries.keys
125
127
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
126
128
  end
@@ -140,7 +142,7 @@ module HTTPX
140
142
  def do_retry
141
143
  return if @queries.empty?
142
144
 
143
- loop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_timeout
145
+ loop_time = Utils.elapsed_time(@start_timeout)
144
146
  connections = []
145
147
  queries = {}
146
148
  while (query = @queries.shift)
@@ -12,6 +12,8 @@ module HTTPX
12
12
  Resolv::DNS::EncodeError,
13
13
  Resolv::DNS::DecodeError].freeze
14
14
 
15
+ attr_reader :state
16
+
15
17
  def initialize(options)
16
18
  @options = Options.new(options)
17
19
  @resolver_options = @options.resolver_options
@@ -26,14 +26,14 @@ module HTTPX
26
26
  module_function
27
27
 
28
28
  def cached_lookup(hostname)
29
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ now = Utils.now
30
30
  @lookup_mutex.synchronize do
31
31
  lookup(hostname, now)
32
32
  end
33
33
  end
34
34
 
35
35
  def cached_lookup_set(hostname, entries)
36
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ now = Utils.now
37
37
  entries.each do |entry|
38
38
  entry["TTL"] += now
39
39
  end
@@ -57,17 +57,23 @@ module HTTPX
57
57
  # :nocov:
58
58
  def inspect
59
59
  "#<Response:#{object_id} "\
60
- "HTTP/#{version} " \
61
- "@status=#{@status} " \
62
- "@headers=#{@headers} " \
63
- "@body=#{@body.bytesize}>"
60
+ "HTTP/#{version} " \
61
+ "@status=#{@status} " \
62
+ "@headers=#{@headers} " \
63
+ "@body=#{@body.bytesize}>"
64
64
  end
65
65
  # :nocov:
66
66
 
67
- def raise_for_status
67
+ def error
68
68
  return if @status < 400
69
69
 
70
- 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
71
77
  end
72
78
 
73
79
  def json(options = nil)
@@ -211,18 +217,20 @@ module HTTPX
211
217
  end
212
218
 
213
219
  def ==(other)
214
- if other.respond_to?(:read)
215
- _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
216
- else
217
- 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
218
226
  end
219
227
  end
220
228
 
221
229
  # :nocov:
222
230
  def inspect
223
231
  "#<HTTPX::Response::Body:#{object_id} " \
224
- "@state=#{@state} " \
225
- "@length=#{@length}>"
232
+ "@state=#{@state} " \
233
+ "@length=#{@length}>"
226
234
  end
227
235
  # :nocov:
228
236
 
@@ -311,17 +319,18 @@ module HTTPX
311
319
  end
312
320
 
313
321
  def status
322
+ warn ":#{__method__} is deprecated, use :error.message instead"
314
323
  @error.message
315
324
  end
316
325
 
317
326
  if Exception.method_defined?(:full_message)
318
327
  def to_s
319
- @error.full_message
328
+ @error.full_message(highlight: false)
320
329
  end
321
330
  else
322
331
  def to_s
323
332
  "#{@error.message} (#{@error.class})\n" \
324
- "#{@error.backtrace.join("\n") if @error.backtrace}"
333
+ "#{@error.backtrace.join("\n") if @error.backtrace}"
325
334
  end
326
335
  end
327
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
@@ -129,6 +117,13 @@ class HTTPX::Selector
129
117
  end
130
118
 
131
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
+
132
127
  return select_one(interval, &block) if @selectables.size == 1
133
128
 
134
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
 
@@ -226,7 +227,11 @@ module HTTPX
226
227
  end
227
228
  responses
228
229
  ensure
229
- close(connections) unless @persistent
230
+ if @persistent
231
+ pool.deactivate(connections)
232
+ else
233
+ close(connections)
234
+ end
230
235
  end
231
236
  end
232
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)
@@ -50,7 +50,7 @@ module HTTPX::Transcoder
50
50
  def decode(response)
51
51
  content_type = response.content_type.mime_type
52
52
 
53
- raise Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
53
+ raise HTTPX::Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
54
54
 
55
55
  Decoder
56
56
  end
@@ -35,7 +35,7 @@ module HTTPX::Transcoder
35
35
  def decode(response)
36
36
  content_type = response.content_type.mime_type
37
37
 
38
- raise Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
38
+ raise HTTPX::Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
39
39
 
40
40
  ::JSON.method(:parse)
41
41
  end
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)
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.17.0"
4
+ VERSION = "0.18.3"
5
5
  end