auth0 5.4.0 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
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