Skip to content

Commit 0b4b24d

Browse files
committed
Merge branch 'dougforpres-auth-retry'
2 parents b9639ee + 8e49ee7 commit 0b4b24d

File tree

4 files changed

+122
-26
lines changed

4 files changed

+122
-26
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ in the credentials. Detailed instructions on how to enable delegation for your d
123123

124124
The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to
125125
the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry.
126+
In the case of authentication token expiry, the API client will attempt to refresh the token and retry the failed operation - this
127+
is a specific exception to the retry rules.
126128

127129
The default value for retries is 0, but will be enabled by default in future releases.
128130

lib/google/api_client.rb

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def initialize(options={})
118118
self.key = options[:key]
119119
self.user_ip = options[:user_ip]
120120
self.retries = options.fetch(:retries) { 0 }
121+
self.expired_auth_retry = options.fetch(:expired_auth_retry) { true }
121122
@discovery_uris = {}
122123
@discovery_documents = {}
123124
@discovered_apis = {}
@@ -255,6 +256,13 @@ def authorization=(new_authorization)
255256
# Number of retries
256257
attr_accessor :retries
257258

259+
##
260+
# Whether or not an expired auth token should be re-acquired
261+
# (and the operation retried) regardless of retries setting
262+
# @return [Boolean]
263+
# Auto retry on auth expiry
264+
attr_accessor :expired_auth_retry
265+
258266
##
259267
# Returns the URI for the directory document.
260268
#
@@ -613,28 +621,40 @@ def execute!(*params)
613621
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
614622

615623
tries = 1 + (options[:retries] || self.retries)
624+
attempt = 0
616625

617626
Retriable.retriable :tries => tries,
618627
:on => [TransmissionError],
619-
:on_retry => client_error_handler(request.authorization),
628+
:on_retry => client_error_handler,
620629
:interval => lambda {|attempts| (2 ** attempts) + rand} do
621-
result = request.send(connection, true)
622-
623-
case result.status
624-
when 200...300
625-
result
626-
when 301, 302, 303, 307
627-
request = generate_request(request.to_hash.merge({
628-
:uri => result.headers['location'],
629-
:api_method => nil
630-
}))
631-
raise RedirectError.new(result.headers['location'], result)
632-
when 400...500
633-
raise ClientError.new(result.error_message || "A client error has occurred", result)
634-
when 500...600
635-
raise ServerError.new(result.error_message || "A server error has occurred", result)
636-
else
637-
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
630+
attempt += 1
631+
632+
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
633+
# auth to be re-attempted without having to retry all sorts of other failures like
634+
# NotFound, etc
635+
Retriable.retriable :tries => ((expired_auth_retry || tries > 1) && attempt == 1) ? 2 : 1,
636+
:on => [AuthorizationError],
637+
:on_retry => authorization_error_handler(request.authorization) do
638+
result = request.send(connection, true)
639+
640+
case result.status
641+
when 200...300
642+
result
643+
when 301, 302, 303, 307
644+
request = generate_request(request.to_hash.merge({
645+
:uri => result.headers['location'],
646+
:api_method => nil
647+
}))
648+
raise RedirectError.new(result.headers['location'], result)
649+
when 401
650+
raise AuthorizationError.new(result.error_message || 'Invalid/Expired Authentication', result)
651+
when 400, 402...500
652+
raise ClientError.new(result.error_message || "A client error has occurred", result)
653+
when 500...600
654+
raise ServerError.new(result.error_message || "A server error has occurred", result)
655+
else
656+
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
657+
end
638658
end
639659
end
640660
end
@@ -682,18 +702,17 @@ def resolve_uri(template, mapping={})
682702

683703

684704
##
685-
# Returns on proc for special processing of retries as not all client errors
686-
# are recoverable. Only 401s should be retried and only if the credentials
687-
# are refreshable
705+
# Returns on proc for special processing of retries for authorization errors
706+
# Only 401s should be retried and only if the credentials are refreshable
688707
#
689708
# @param [#fetch_access_token!] authorization
690709
# OAuth 2 credentials
691-
# @return [Proc]
692-
def client_error_handler(authorization)
710+
# @return [Proc]
711+
def authorization_error_handler(authorization)
693712
can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token
694713
Proc.new do |exception, tries|
695-
next unless exception.kind_of?(ClientError)
696-
if exception.result.status == 401 && can_refresh && tries == 1
714+
next unless exception.kind_of?(AuthorizationError)
715+
if can_refresh
697716
begin
698717
logger.debug("Attempting refresh of access token & retry of request")
699718
authorization.fetch_access_token!
@@ -705,6 +724,17 @@ def client_error_handler(authorization)
705724
end
706725
end
707726

727+
##
728+
# Returns on proc for special processing of retries as not all client errors
729+
# are recoverable. Only 401s should be retried (via authorization_error_handler)
730+
#
731+
# @return [Proc]
732+
def client_error_handler
733+
Proc.new do |exception, tries|
734+
raise exception if exception.kind_of?(ClientError)
735+
end
736+
end
737+
708738
end
709739

710740
end

lib/google/api_client/errors.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class ValidationError < StandardError
4343
class ClientError < TransmissionError
4444
end
4545

46+
##
47+
# A 401 HTTP error occurred.
48+
class AuthorizationError < ClientError
49+
end
50+
4651
##
4752
# A 5xx class HTTP error occurred.
4853
class ServerError < TransmissionError

spec/google/api_client_spec.rb

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@
200200
)
201201
end
202202

203-
it 'should refresh tokens on 401 tokens' do
203+
it 'should refresh tokens on 401 errors' do
204204
client.authorization.access_token = '12345'
205205
expect(client.authorization).to receive(:fetch_access_token!)
206206

@@ -290,4 +290,63 @@
290290
end
291291

292292
end
293+
294+
describe 'when retries disabled and expired_auth_retry on (default)' do
295+
before do
296+
client.retries = 0
297+
end
298+
299+
after do
300+
@connection.verify
301+
end
302+
303+
it 'should refresh tokens on 401 errors' do
304+
client.authorization.access_token = '12345'
305+
expect(client.authorization).to receive(:fetch_access_token!)
306+
307+
@connection = stub_connection do |stub|
308+
stub.get('/foo') do |env|
309+
[401, {}, '{}']
310+
end
311+
stub.get('/foo') do |env|
312+
[200, {}, '{}']
313+
end
314+
end
315+
316+
client.execute(
317+
:uri => 'https://www.gogole.com/foo',
318+
:connection => @connection
319+
)
320+
end
321+
322+
end
323+
324+
describe 'when retries disabled and expired_auth_retry off' do
325+
before do
326+
client.retries = 0
327+
client.expired_auth_retry = false
328+
end
329+
330+
it 'should not refresh tokens on 401 errors' do
331+
client.authorization.access_token = '12345'
332+
expect(client.authorization).not_to receive(:fetch_access_token!)
333+
334+
@connection = stub_connection do |stub|
335+
stub.get('/foo') do |env|
336+
[401, {}, '{}']
337+
end
338+
stub.get('/foo') do |env|
339+
[200, {}, '{}']
340+
end
341+
end
342+
343+
resp = client.execute(
344+
:uri => 'https://www.gogole.com/foo',
345+
:connection => @connection
346+
)
347+
348+
expect(resp.response.status).to be == 401
349+
end
350+
351+
end
293352
end

0 commit comments

Comments
 (0)