auth0 5.4.0 → 5.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc12c7c72ce796f0bc1a7599037731d8561438b610c45c7401e164f5c0ffa78e
4
- data.tar.gz: a821ee4bccbaa05ecef61d586c36d6088318eaf28f254ee78f387740b4949725
3
+ metadata.gz: a3d59041ebfdd2dfcbc326ea1ea9e416796afd2818607014757d90266ed16c07
4
+ data.tar.gz: e9e950591d2027fc475fa437f144dea929a724eeea94bd9491b0a0af5095b271
5
5
  SHA512:
6
- metadata.gz: 758ede4765b01d316a9dab22abc0d4ecc315d1e743e8700d5451f61f70e4f8d57c5a3561dfe96f0732546139ff39696e12e16d5ff20daf5e168f726ca1c1d571
7
- data.tar.gz: f29e16f61158cbc0651a0b76429032c61c5cde9006ce267d008d9f7ac5ce91136a575e64f419be937df7add6e0f36e7e88e5ea3afa2ea3a654d8c2d7123ef1b2
6
+ metadata.gz: 4f6733b62fa9839d0c03f4a9b641c5f86ffd3a47b7961d76156881f95f34a5ca0cf63c765fccc124418affaca78f34d72e899d5592eddf690f18d4d939b3f9fe
7
+ data.tar.gz: b9bf5d5f1e570a595388fbd3555ca23d6964c6a445d150b233f51dd2a19da30ef4a906825567da350a6411adbdee0a2af562953a3034e41f0c714a0ad0a4d04a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## [v5.5.0](https://github.com/auth0/ruby-auth0/tree/v5.5.0) (2021-08-06)
4
+
5
+ [Full Changelog](https://github.com/auth0/ruby-auth0/compare/v5.4.0..v5.5.0)
6
+
7
+ **Added**
8
+
9
+ - Automatically retry requests when API returns a 429 rate-limit status header. [\#290](https://github.com/auth0/ruby-auth0/pull/290) ([davidpatrick](https://github.com/davidpatrick))
10
+
3
11
  ## [v5.4.0](https://github.com/auth0/ruby-auth0/tree/v5.4.0) (2021-07-23)
4
12
 
5
13
  [Full Changelog](https://github.com/auth0/ruby-auth0/compare/v5.3.0..v5.4.0)
data/auth0.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.add_runtime_dependency 'jwt', '~> 2.2'
21
21
  s.add_runtime_dependency 'zache', '~> 0.12'
22
22
  s.add_runtime_dependency 'addressable', '~> 2.8'
23
+ s.add_runtime_dependency 'retryable', '~> 3.0'
23
24
 
24
25
  s.add_development_dependency 'bundler'
25
26
  s.add_development_dependency 'rake', '~> 13.0'
@@ -1,56 +1,52 @@
1
1
  require "addressable/uri"
2
+ require "retryable"
3
+ require_relative "../exception.rb"
2
4
 
3
5
  module Auth0
4
6
  module Mixins
5
7
  # here's the proxy for Rest calls based on rest-client, we're building all request on that gem
6
8
  # for now, if you want to feel free to use your own http client
7
9
  module HTTPProxy
8
- attr_accessor :headers, :base_uri, :timeout
10
+ attr_accessor :headers, :base_uri, :timeout, :retry_count
11
+ DEAFULT_RETRIES = 3
12
+ MAX_ALLOWED_RETRIES = 10
13
+ MAX_REQUEST_RETRY_JITTER = 250
14
+ MAX_REQUEST_RETRY_DELAY = 1000
15
+ MIN_REQUEST_RETRY_DELAY = 100
9
16
 
10
17
  # proxying requests from instance methods to HTTP class methods
11
18
  %i(get post post_file put patch delete delete_with_body).each do |method|
12
19
  define_method(method) do |uri, body = {}, extra_headers = {}|
20
+ body = body.delete_if { |_, v| v.nil? }
13
21
 
14
- if base_uri
15
- # if a base_uri is set then the uri can be encoded as a path
16
- safe_path = Addressable::URI.new(path: uri).normalized_path
17
- else
18
- safe_path = Addressable::URI.escape(uri)
22
+ Retryable.retryable(retry_options) do
23
+ request(method, uri, body, extra_headers)
19
24
  end
25
+ end
26
+ end
20
27
 
21
- body = body.delete_if { |_, v| v.nil? }
22
- result = if method == :get
23
- # Mutate the headers property to add parameters.
24
- add_headers({params: body})
25
- # Merge custom headers into existing ones for this req.
26
- # This prevents future calls from using them.
27
- get_headers = headers.merge extra_headers
28
- # Make the call with extra_headers, if provided.
29
- call(:get, url(safe_path), timeout, get_headers)
30
- elsif method == :delete
31
- call(:delete, url(safe_path), timeout, add_headers({params: body}))
32
- elsif method == :delete_with_body
33
- call(:delete, url(safe_path), timeout, headers, body.to_json)
34
- elsif method == :post_file
35
- body.merge!(multipart: true)
36
- # Ignore the default Content-Type headers and let the HTTP client define them
37
- post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
38
- # Actual call with the altered headers
39
- call(:post, url(safe_path), timeout, post_file_headers, body)
40
- else
41
- call(method, url(safe_path), timeout, headers, body.to_json)
42
- end
43
- case result.code
44
- when 200...226 then safe_parse_json(result.body)
45
- when 400 then raise Auth0::BadRequest.new(result.body, code: result.code, headers: result.headers)
46
- when 401 then raise Auth0::Unauthorized.new(result.body, code: result.code, headers: result.headers)
47
- when 403 then raise Auth0::AccessDenied.new(result.body, code: result.code, headers: result.headers)
48
- when 404 then raise Auth0::NotFound.new(result.body, code: result.code, headers: result.headers)
49
- when 429 then raise Auth0::RateLimitEncountered.new(result.body, code: result.code, headers: result.headers)
50
- when 500 then raise Auth0::ServerError.new(result.body, code: result.code, headers: result.headers)
51
- else raise Auth0::Unsupported.new(result.body, code: result.code, headers: result.headers)
52
- end
28
+ def retry_options
29
+ sleep_timer = lambda do |attempt|
30
+ wait = 1000 * 2**attempt # Exponential delay with each subsequent request attempt.
31
+ wait += rand(wait..wait+MAX_REQUEST_RETRY_JITTER) # Add jitter to the delay window.
32
+ wait = [MAX_REQUEST_RETRY_DELAY, wait].min # Cap delay at MAX_REQUEST_RETRY_DELAY.
33
+ wait = [MIN_REQUEST_RETRY_DELAY, wait].max # Ensure delay is no less than MIN_REQUEST_RETRY_DELAY.
34
+ wait / 1000.to_f.round(2) # convert ms to seconds
53
35
  end
36
+
37
+ tries = 1 + [Integer(retry_count || DEAFULT_RETRIES), MAX_ALLOWED_RETRIES].min # Cap retries at MAX_ALLOWED_RETRIES
38
+
39
+ {
40
+ tries: tries,
41
+ sleep: sleep_timer,
42
+ on: Auth0::RateLimitEncountered
43
+ }
44
+ end
45
+
46
+ def encode_uri(uri)
47
+ # if a base_uri is set then the uri can be encoded as a path
48
+ path = base_uri ? Addressable::URI.new(path: uri).normalized_path : Addressable::URI.escape(uri)
49
+ url(path)
54
50
  end
55
51
 
56
52
  def url(path)
@@ -69,6 +65,41 @@ module Auth0
69
65
  body
70
66
  end
71
67
 
68
+ def request(method, uri, body, extra_headers)
69
+ result = if method == :get
70
+ # Mutate the headers property to add parameters.
71
+ add_headers({params: body})
72
+ # Merge custom headers into existing ones for this req.
73
+ # This prevents future calls from using them.
74
+ get_headers = headers.merge extra_headers
75
+ # Make the call with extra_headers, if provided.
76
+ call(:get, encode_uri(uri), timeout, get_headers)
77
+ elsif method == :delete
78
+ call(:delete, encode_uri(uri), timeout, add_headers({params: body}))
79
+ elsif method == :delete_with_body
80
+ call(:delete, encode_uri(uri), timeout, headers, body.to_json)
81
+ elsif method == :post_file
82
+ body.merge!(multipart: true)
83
+ # Ignore the default Content-Type headers and let the HTTP client define them
84
+ post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
85
+ # Actual call with the altered headers
86
+ call(:post, encode_uri(uri), timeout, post_file_headers, body)
87
+ else
88
+ call(method, encode_uri(uri), timeout, headers, body.to_json)
89
+ end
90
+
91
+ case result.code
92
+ when 200...226 then safe_parse_json(result.body)
93
+ when 400 then raise Auth0::BadRequest.new(result.body, code: result.code, headers: result.headers)
94
+ when 401 then raise Auth0::Unauthorized.new(result.body, code: result.code, headers: result.headers)
95
+ when 403 then raise Auth0::AccessDenied.new(result.body, code: result.code, headers: result.headers)
96
+ when 404 then raise Auth0::NotFound.new(result.body, code: result.code, headers: result.headers)
97
+ when 429 then raise Auth0::RateLimitEncountered.new(result.body, code: result.code, headers: result.headers)
98
+ when 500 then raise Auth0::ServerError.new(result.body, code: result.code, headers: result.headers)
99
+ else raise Auth0::Unsupported.new(result.body, code: result.code, headers: result.headers)
100
+ end
101
+ end
102
+
72
103
  def call(method, url, timeout, headers, body = nil)
73
104
  RestClient::Request.execute(
74
105
  method: method,
@@ -15,6 +15,7 @@ module Auth0
15
15
  @base_uri = base_url(options)
16
16
  @headers = client_headers
17
17
  @timeout = options[:timeout] || 10
18
+ @retry_count = options[:retry_count]
18
19
  extend Auth0::Api::AuthenticationEndpoints
19
20
  @client_id = options[:client_id]
20
21
  @client_secret = options[:client_secret]
data/lib/auth0/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # current version of gem
2
2
  module Auth0
3
- VERSION = '5.4.0'.freeze
3
+ VERSION = '5.5.0'.freeze
4
4
  end
@@ -6,6 +6,7 @@ describe Auth0::Mixins::HTTPProxy do
6
6
  dummy_instance = DummyClassForProxy.new
7
7
  dummy_instance.extend(Auth0::Mixins::HTTPProxy)
8
8
  dummy_instance.base_uri = "https://auth0.com"
9
+ dummy_instance.retry_count = 0
9
10
 
10
11
  @instance = dummy_instance
11
12
  @exception = DummyClassForRestClient.new
@@ -152,6 +153,100 @@ describe Auth0::Mixins::HTTPProxy do
152
153
  .and_return(StubResponse.new({}, true, 200))
153
154
  expect { @instance.send(http_method, '/te st#test') }.not_to raise_error
154
155
  end
156
+
157
+ context "when status 429 is recieved on send http #{http_method} method" do
158
+ it "should retry 3 times when retry_count is not set" do
159
+ retry_instance = DummyClassForProxy.new
160
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
161
+ retry_instance.base_uri = "https://auth0.com"
162
+
163
+ @exception.response = StubResponse.new({}, false, 429)
164
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
165
+ url: 'https://auth0.com/test',
166
+ timeout: nil,
167
+ headers: { params: {} },
168
+ payload: nil)
169
+ .and_raise(@exception)
170
+ expect(RestClient::Request).to receive(:execute).exactly(4).times
171
+
172
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
173
+ expect(error).to be_a(Auth0::RateLimitEncountered)
174
+ }
175
+ end
176
+
177
+ it "should retry 2 times when retry_count is set to 2" do
178
+ retry_instance = DummyClassForProxy.new
179
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
180
+ retry_instance.base_uri = "https://auth0.com"
181
+ retry_instance.retry_count = 2
182
+
183
+ @exception.response = StubResponse.new({}, false, 429)
184
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
185
+ url: 'https://auth0.com/test',
186
+ timeout: nil,
187
+ headers: { params: {} },
188
+ payload: nil)
189
+ .and_raise(@exception)
190
+ expect(RestClient::Request).to receive(:execute).exactly(3).times
191
+
192
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
193
+ expect(error).to be_a(Auth0::RateLimitEncountered)
194
+ }
195
+ end
196
+
197
+ it "should not retry when retry_count is set to 0" do
198
+ retry_instance = DummyClassForProxy.new
199
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
200
+ retry_instance.base_uri = "https://auth0.com"
201
+ retry_instance.retry_count = 0
202
+
203
+ @exception.response = StubResponse.new({}, false, 429)
204
+
205
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
206
+ url: 'https://auth0.com/test',
207
+ timeout: nil,
208
+ headers: { params: {} },
209
+ payload: nil)
210
+ .and_raise(@exception)
211
+
212
+ expect(RestClient::Request).to receive(:execute).exactly(1).times
213
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
214
+ expect(error).to be_a(Auth0::RateLimitEncountered)
215
+ }
216
+ end
217
+
218
+ it "should have have random retry times grow with jitter backoff" do
219
+ retry_instance = DummyClassForProxy.new
220
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
221
+ retry_instance.base_uri = "https://auth0.com"
222
+ retry_instance.retry_count = 2
223
+ time_entries = []
224
+ @time_start
225
+
226
+ @exception.response = StubResponse.new({}, false, 429)
227
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
228
+ url: 'https://auth0.com/test',
229
+ timeout: nil,
230
+ headers: { params: {} },
231
+ payload: nil) do
232
+
233
+ time_entries.push(Time.now.to_f - @time_start.to_f)
234
+ @time_start = Time.now.to_f # restart the clock
235
+ raise @exception
236
+ end
237
+
238
+ @time_start = Time.now.to_f #start the clock
239
+ retry_instance.send(http_method, '/test') rescue nil
240
+ time_entries_first_set = time_entries.shift(time_entries.length)
241
+
242
+ retry_instance.send(http_method, '/test') rescue nil
243
+ time_entries.each_with_index do |entry, index|
244
+ if index > 0 #skip the first request
245
+ expect(entry != time_entries_first_set[index])
246
+ end
247
+ end
248
+ end
249
+ end
155
250
  end
156
251
  end
157
252
 
@@ -301,6 +396,100 @@ describe Auth0::Mixins::HTTPProxy do
301
396
  .and_return(StubResponse.new(res, true, 404))
302
397
  expect { @instance.send(http_method, '/test') }.to raise_error(Auth0::NotFound, res)
303
398
  end
399
+
400
+ context "when status 429 is recieved on send http #{http_method} method" do
401
+ it "should retry 3 times when retry_count is not set" do
402
+ retry_instance = DummyClassForProxy.new
403
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
404
+ retry_instance.base_uri = "https://auth0.com"
405
+
406
+ @exception.response = StubResponse.new({}, false, 429)
407
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
408
+ url: 'https://auth0.com/test',
409
+ timeout: nil,
410
+ headers: nil,
411
+ payload: '{}')
412
+ .and_raise(@exception)
413
+ expect(RestClient::Request).to receive(:execute).exactly(4).times
414
+
415
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
416
+ expect(error).to be_a(Auth0::RateLimitEncountered)
417
+ }
418
+ end
419
+
420
+ it "should retry 2 times when retry_count is set to 2" do
421
+ retry_instance = DummyClassForProxy.new
422
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
423
+ retry_instance.base_uri = "https://auth0.com"
424
+ retry_instance.retry_count = 2
425
+
426
+ @exception.response = StubResponse.new({}, false, 429)
427
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
428
+ url: 'https://auth0.com/test',
429
+ timeout: nil,
430
+ headers: nil,
431
+ payload: '{}')
432
+ .and_raise(@exception)
433
+ expect(RestClient::Request).to receive(:execute).exactly(3).times
434
+
435
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
436
+ expect(error).to be_a(Auth0::RateLimitEncountered)
437
+ }
438
+ end
439
+
440
+ it "should not retry when retry_count is set to 0" do
441
+ retry_instance = DummyClassForProxy.new
442
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
443
+ retry_instance.base_uri = "https://auth0.com"
444
+ retry_instance.retry_count = 0
445
+
446
+ @exception.response = StubResponse.new({}, false, 429)
447
+
448
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
449
+ url: 'https://auth0.com/test',
450
+ timeout: nil,
451
+ headers: nil,
452
+ payload: '{}')
453
+ .and_raise(@exception)
454
+
455
+ expect(RestClient::Request).to receive(:execute).exactly(1).times
456
+ expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
457
+ expect(error).to be_a(Auth0::RateLimitEncountered)
458
+ }
459
+ end
460
+
461
+ it "should have have random retry times grow with jitter backoff" do
462
+ retry_instance = DummyClassForProxy.new
463
+ retry_instance.extend(Auth0::Mixins::HTTPProxy)
464
+ retry_instance.base_uri = "https://auth0.com"
465
+ retry_instance.retry_count = 2
466
+ time_entries = []
467
+ @time_start
468
+
469
+ @exception.response = StubResponse.new({}, false, 429)
470
+ allow(RestClient::Request).to receive(:execute).with(method: http_method,
471
+ url: 'https://auth0.com/test',
472
+ timeout: nil,
473
+ headers: nil,
474
+ payload: '{}') do
475
+
476
+ time_entries.push(Time.now.to_f - @time_start.to_f)
477
+ @time_start = Time.now.to_f # restart the clock
478
+ raise @exception
479
+ end
480
+
481
+ @time_start = Time.now.to_f #start the clock
482
+ retry_instance.send(http_method, '/test') rescue nil
483
+ time_entries_first_set = time_entries.shift(time_entries.length)
484
+
485
+ retry_instance.send(http_method, '/test') rescue nil
486
+ time_entries.each_with_index do |entry, index|
487
+ if index > 0 #skip the first request
488
+ expect(entry != time_entries_first_set[index])
489
+ end
490
+ end
491
+ end
492
+ end
304
493
  end
305
494
  end
306
495
  end
@@ -26,5 +26,12 @@ describe Auth0::Mixins::Initializer do
26
26
 
27
27
  expect(instance.instance_variable_get('@token')).to eq('123')
28
28
  end
29
+
30
+ it 'sets retry_count when passed' do
31
+ params[:token] = '123'
32
+ params[:retry_count] = 10
33
+
34
+ expect(instance.instance_variable_get('@retry_count')).to eq(10)
35
+ end
29
36
  end
30
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: auth0
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Auth0
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-07-30 00:00:00.000000000 Z
14
+ date: 2021-08-07 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rest-client
@@ -69,6 +69,20 @@ dependencies:
69
69
  - - "~>"
70
70
  - !ruby/object:Gem::Version
71
71
  version: '2.8'
72
+ - !ruby/object:Gem::Dependency
73
+ name: retryable
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '3.0'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '3.0'
72
86
  - !ruby/object:Gem::Dependency
73
87
  name: bundler
74
88
  requirement: !ruby/object:Gem::Requirement