Merge branch 'dougforpres-auth-retry'
This commit is contained in:
commit
0b4b24d06d
|
@ -123,6 +123,8 @@ in the credentials. Detailed instructions on how to enable delegation for your d
|
||||||
|
|
||||||
The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to
|
The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to
|
||||||
the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry.
|
the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry.
|
||||||
|
In the case of authentication token expiry, the API client will attempt to refresh the token and retry the failed operation - this
|
||||||
|
is a specific exception to the retry rules.
|
||||||
|
|
||||||
The default value for retries is 0, but will be enabled by default in future releases.
|
The default value for retries is 0, but will be enabled by default in future releases.
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ module Google
|
||||||
self.key = options[:key]
|
self.key = options[:key]
|
||||||
self.user_ip = options[:user_ip]
|
self.user_ip = options[:user_ip]
|
||||||
self.retries = options.fetch(:retries) { 0 }
|
self.retries = options.fetch(:retries) { 0 }
|
||||||
|
self.expired_auth_retry = options.fetch(:expired_auth_retry) { true }
|
||||||
@discovery_uris = {}
|
@discovery_uris = {}
|
||||||
@discovery_documents = {}
|
@discovery_documents = {}
|
||||||
@discovered_apis = {}
|
@discovered_apis = {}
|
||||||
|
@ -255,6 +256,13 @@ module Google
|
||||||
# Number of retries
|
# Number of retries
|
||||||
attr_accessor :retries
|
attr_accessor :retries
|
||||||
|
|
||||||
|
##
|
||||||
|
# Whether or not an expired auth token should be re-acquired
|
||||||
|
# (and the operation retried) regardless of retries setting
|
||||||
|
# @return [Boolean]
|
||||||
|
# Auto retry on auth expiry
|
||||||
|
attr_accessor :expired_auth_retry
|
||||||
|
|
||||||
##
|
##
|
||||||
# Returns the URI for the directory document.
|
# Returns the URI for the directory document.
|
||||||
#
|
#
|
||||||
|
@ -613,11 +621,20 @@ module Google
|
||||||
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
|
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
|
||||||
|
|
||||||
tries = 1 + (options[:retries] || self.retries)
|
tries = 1 + (options[:retries] || self.retries)
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
Retriable.retriable :tries => tries,
|
Retriable.retriable :tries => tries,
|
||||||
:on => [TransmissionError],
|
:on => [TransmissionError],
|
||||||
:on_retry => client_error_handler(request.authorization),
|
:on_retry => client_error_handler,
|
||||||
:interval => lambda {|attempts| (2 ** attempts) + rand} do
|
:interval => lambda {|attempts| (2 ** attempts) + rand} do
|
||||||
|
attempt += 1
|
||||||
|
|
||||||
|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
|
||||||
|
# auth to be re-attempted without having to retry all sorts of other failures like
|
||||||
|
# NotFound, etc
|
||||||
|
Retriable.retriable :tries => ((expired_auth_retry || tries > 1) && attempt == 1) ? 2 : 1,
|
||||||
|
:on => [AuthorizationError],
|
||||||
|
:on_retry => authorization_error_handler(request.authorization) do
|
||||||
result = request.send(connection, true)
|
result = request.send(connection, true)
|
||||||
|
|
||||||
case result.status
|
case result.status
|
||||||
|
@ -629,7 +646,9 @@ module Google
|
||||||
:api_method => nil
|
:api_method => nil
|
||||||
}))
|
}))
|
||||||
raise RedirectError.new(result.headers['location'], result)
|
raise RedirectError.new(result.headers['location'], result)
|
||||||
when 400...500
|
when 401
|
||||||
|
raise AuthorizationError.new(result.error_message || 'Invalid/Expired Authentication', result)
|
||||||
|
when 400, 402...500
|
||||||
raise ClientError.new(result.error_message || "A client error has occurred", result)
|
raise ClientError.new(result.error_message || "A client error has occurred", result)
|
||||||
when 500...600
|
when 500...600
|
||||||
raise ServerError.new(result.error_message || "A server error has occurred", result)
|
raise ServerError.new(result.error_message || "A server error has occurred", result)
|
||||||
|
@ -638,6 +657,7 @@ module Google
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Same as Google::APIClient#execute!, but does not raise an exception for
|
# Same as Google::APIClient#execute!, but does not raise an exception for
|
||||||
|
@ -682,18 +702,17 @@ module Google
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Returns on proc for special processing of retries as not all client errors
|
# Returns on proc for special processing of retries for authorization errors
|
||||||
# are recoverable. Only 401s should be retried and only if the credentials
|
# Only 401s should be retried and only if the credentials are refreshable
|
||||||
# are refreshable
|
|
||||||
#
|
#
|
||||||
# @param [#fetch_access_token!] authorization
|
# @param [#fetch_access_token!] authorization
|
||||||
# OAuth 2 credentials
|
# OAuth 2 credentials
|
||||||
# @return [Proc]
|
# @return [Proc]
|
||||||
def client_error_handler(authorization)
|
def authorization_error_handler(authorization)
|
||||||
can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token
|
can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token
|
||||||
Proc.new do |exception, tries|
|
Proc.new do |exception, tries|
|
||||||
next unless exception.kind_of?(ClientError)
|
next unless exception.kind_of?(AuthorizationError)
|
||||||
if exception.result.status == 401 && can_refresh && tries == 1
|
if can_refresh
|
||||||
begin
|
begin
|
||||||
logger.debug("Attempting refresh of access token & retry of request")
|
logger.debug("Attempting refresh of access token & retry of request")
|
||||||
authorization.fetch_access_token!
|
authorization.fetch_access_token!
|
||||||
|
@ -705,6 +724,17 @@ module Google
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns on proc for special processing of retries as not all client errors
|
||||||
|
# are recoverable. Only 401s should be retried (via authorization_error_handler)
|
||||||
|
#
|
||||||
|
# @return [Proc]
|
||||||
|
def client_error_handler
|
||||||
|
Proc.new do |exception, tries|
|
||||||
|
raise exception if exception.kind_of?(ClientError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,6 +43,11 @@ module Google
|
||||||
class ClientError < TransmissionError
|
class ClientError < TransmissionError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# A 401 HTTP error occurred.
|
||||||
|
class AuthorizationError < ClientError
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# A 5xx class HTTP error occurred.
|
# A 5xx class HTTP error occurred.
|
||||||
class ServerError < TransmissionError
|
class ServerError < TransmissionError
|
||||||
|
|
|
@ -200,7 +200,7 @@ RSpec.describe Google::APIClient do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should refresh tokens on 401 tokens' do
|
it 'should refresh tokens on 401 errors' do
|
||||||
client.authorization.access_token = '12345'
|
client.authorization.access_token = '12345'
|
||||||
expect(client.authorization).to receive(:fetch_access_token!)
|
expect(client.authorization).to receive(:fetch_access_token!)
|
||||||
|
|
||||||
|
@ -290,4 +290,63 @@ RSpec.describe Google::APIClient do
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'when retries disabled and expired_auth_retry on (default)' do
|
||||||
|
before do
|
||||||
|
client.retries = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
@connection.verify
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should refresh tokens on 401 errors' do
|
||||||
|
client.authorization.access_token = '12345'
|
||||||
|
expect(client.authorization).to receive(:fetch_access_token!)
|
||||||
|
|
||||||
|
@connection = stub_connection do |stub|
|
||||||
|
stub.get('/foo') do |env|
|
||||||
|
[401, {}, '{}']
|
||||||
|
end
|
||||||
|
stub.get('/foo') do |env|
|
||||||
|
[200, {}, '{}']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
:uri => 'https://www.gogole.com/foo',
|
||||||
|
:connection => @connection
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when retries disabled and expired_auth_retry off' do
|
||||||
|
before do
|
||||||
|
client.retries = 0
|
||||||
|
client.expired_auth_retry = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not refresh tokens on 401 errors' do
|
||||||
|
client.authorization.access_token = '12345'
|
||||||
|
expect(client.authorization).not_to receive(:fetch_access_token!)
|
||||||
|
|
||||||
|
@connection = stub_connection do |stub|
|
||||||
|
stub.get('/foo') do |env|
|
||||||
|
[401, {}, '{}']
|
||||||
|
end
|
||||||
|
stub.get('/foo') do |env|
|
||||||
|
[200, {}, '{}']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resp = client.execute(
|
||||||
|
:uri => 'https://www.gogole.com/foo',
|
||||||
|
:connection => @connection
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resp.response.status).to be == 401
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue