Skip to content

Commit 1819e16

Browse files
committed
Add retry support & redirect following
1 parent 30881d7 commit 1819e16

File tree

6 files changed

+173
-33
lines changed

6 files changed

+173
-33
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ gem 'faraday', '>= 0.9.0'
1010
gem 'multi_json', '>= 1.0.0'
1111
gem 'extlib', '>= 0.9.15'
1212
gem 'jwt', '~> 0.1.5'
13+
gem 'retriable', '>= 1.4'
1314
gem 'jruby-openssl', :platforms => :jruby
1415

1516
group :development do

lib/google/api_client.rb

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
require 'multi_json'
1818
require 'compat/multi_json'
1919
require 'stringio'
20+
require 'retriable'
2021

2122
require 'google/api_client/version'
2223
require 'google/api_client/logging'
@@ -107,6 +108,7 @@ def initialize(options={})
107108
self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
108109
self.key = options[:key]
109110
self.user_ip = options[:user_ip]
111+
self.retries = options.fetch(:retries) { 0 }
110112
@discovery_uris = {}
111113
@discovery_documents = {}
112114
@discovered_apis = {}
@@ -230,6 +232,13 @@ def authorization=(new_authorization)
230232
# The base path. Should almost always be '/discovery/v1'.
231233
attr_accessor :discovery_path
232234

235+
##
236+
# Number of times to retry on recoverable errors
237+
#
238+
# @return [FixNum]
239+
# Number of retries
240+
attr_accessor :retries
241+
233242
##
234243
# Returns the URI for the directory document.
235244
#
@@ -424,6 +433,8 @@ def preferred_version(api)
424433
# Verifies an ID token against a server certificate. Used to ensure that
425434
# an ID token supplied by an untrusted client-side mechanism is valid.
426435
# Raises an error if the token is invalid or missing.
436+
#
437+
# @deprecated Use the google-id-token gem for verifying JWTs
427438
def verify_id_token!
428439
require 'jwt'
429440
require 'openssl'
@@ -533,6 +544,8 @@ def generate_request(options={})
533544
# authenticated, `false` otherwise.
534545
# - (TrueClass, FalseClass) :gzip (default: true) -
535546
# `true` if gzip enabled, `false` otherwise.
547+
# - (FixNum) :retries -
548+
# # of times to retry on recoverable errors
536549
#
537550
# @return [Google::APIClient::Result] The result from the API, nil if batch.
538551
#
@@ -547,7 +560,7 @@ def generate_request(options={})
547560
# )
548561
#
549562
# @see Google::APIClient#generate_request
550-
def execute(*params)
563+
def execute!(*params)
551564
if params.first.kind_of?(Google::APIClient::Request)
552565
request = params.shift
553566
options = params.shift || {}
@@ -572,53 +585,60 @@ def execute(*params)
572585

573586
request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
574587
request.headers['Accept-Encoding'] ||= 'gzip' unless options[:gzip] == false
588+
request.headers['Content-Type'] ||= ''
575589
request.parameters['key'] ||= self.key unless self.key.nil?
576590
request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
577591

578592
connection = options[:connection] || self.connection
579593
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
580-
581-
result = request.send(connection)
582-
if result.status == 401 && request.authorization.respond_to?(:refresh_token) && auto_refresh_token
583-
begin
584-
logger.debug("Attempting refresh of access token & retry of request")
585-
request.authorization.fetch_access_token!
586-
result = request.send(connection, true)
587-
rescue Signet::AuthorizationError
588-
# Ignore since we want the original error
594+
tries = 1 + (options[:retries] || self.retries)
595+
Retriable.retriable :tries => tries,
596+
:on => [TransmissionError],
597+
:interval => lambda {|attempts| (2 ** attempts) + Random.rand} do
598+
result = request.send(connection, true)
599+
600+
case result.status
601+
when 200...300
602+
result
603+
when 301, 302, 303, 307
604+
request = generate_request(request.to_hash.merge({
605+
:uri => result.headers['location'],
606+
:api_method => nil
607+
}))
608+
raise RedirectError.new(result.headers['location'], result)
609+
when 400...500
610+
if result.status == 401 && request.authorization.respond_to?(:refresh_token) && auto_refresh_token
611+
begin
612+
logger.debug("Attempting refresh of access token & retry of request")
613+
request.authorization.fetch_access_token!
614+
rescue Signet::AuthorizationError
615+
# Ignore since we want the original error
616+
end
617+
end
618+
raise ClientError.new(result.error_message || "A client error has occurred", result)
619+
when 500...600
620+
raise ServerError.new(result.error_message || "A server error has occurred", result)
621+
else
622+
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
589623
end
590624
end
591-
592-
return result
593625
end
594626

595627
##
596-
# Same as Google::APIClient#execute, but raises an exception if there was
597-
# an error.
628+
# Same as Google::APIClient#execute!, but does not raise an exception for
629+
# normal API errros.
598630
#
599631
# @see Google::APIClient#execute
600-
def execute!(*params)
601-
result = self.execute(*params)
602-
if result.error?
603-
error_message = result.error_message
604-
case result.response.status
605-
when 400...500
606-
exception_type = ClientError
607-
error_message ||= "A client error has occurred."
608-
when 500...600
609-
exception_type = ServerError
610-
error_message ||= "A server error has occurred."
611-
else
612-
exception_type = TransmissionError
613-
error_message ||= "A transmission error has occurred."
614-
end
615-
raise exception_type, error_message
632+
def execute(*params)
633+
begin
634+
return self.execute!(*params)
635+
rescue TransmissionError => e
636+
return e.result
616637
end
617-
return result
618638
end
619-
639+
620640
protected
621-
641+
622642
##
623643
# Resolves a URI template against the client's configured base.
624644
#
@@ -646,6 +666,7 @@ def resolve_uri(template, mapping={})
646666
end
647667

648668
end
669+
649670
end
650671

651672
require 'google/api_client/version'

lib/google/api_client/errors.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ class APIClient
1919
# An error which is raised when there is an unexpected response or other
2020
# transport error that prevents an operation from succeeding.
2121
class TransmissionError < StandardError
22+
attr_reader :result
23+
def initialize(message = nil, result = nil)
24+
super(message)
25+
@result = result
26+
end
27+
end
28+
29+
##
30+
# An exception that is raised if a redirect is required
31+
#
32+
class RedirectError < TransmissionError
2233
end
2334

2435
##

spec/google/api_client/discovery_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989

9090
conn = stub_connection do |stub|
9191
stub.get('/discovery/v1/apis/prediction/v1.2/rest?userIp=127.0.0.1') do |env|
92+
[200, {}, '{}']
9293
end
9394
end
9495
CLIENT.execute(
@@ -104,6 +105,7 @@
104105
CLIENT.key = 'qwerty'
105106
conn = stub_connection do |stub|
106107
stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty') do |env|
108+
[200, {}, '{}']
107109
end
108110
end
109111
request = CLIENT.execute(
@@ -120,6 +122,7 @@
120122
CLIENT.user_ip = '127.0.0.1'
121123
conn = stub_connection do |stub|
122124
stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty&userIp=127.0.0.1') do |env|
125+
[200, {}, '{}']
123126
end
124127
end
125128
request = CLIENT.execute(
@@ -187,6 +190,7 @@
187190
conn = stub_connection do |stub|
188191
stub.post('/prediction/v1.2/training?data=12345') do |env|
189192
env[:body].should == ''
193+
[200, {}, '{}']
190194
end
191195
end
192196
request = CLIENT.execute(
@@ -204,6 +208,7 @@
204208
# to a CGI-escaped semicolon (%3B) instead.
205209
stub.post('/prediction/v1.2/training?data=12345%3B67890') do |env|
206210
env[:body].should == ''
211+
[200, {}, '{}']
207212
end
208213
end
209214
request = CLIENT.execute(
@@ -218,6 +223,7 @@
218223
conn = stub_connection do |stub|
219224
stub.post('/prediction/v1.2/training?data=1&data=2') do |env|
220225
env.params['data'].should include('1', '2')
226+
[200, {}, '{}']
221227
end
222228
end
223229
request = CLIENT.execute(
@@ -231,6 +237,7 @@
231237
it 'should generate requests against the correct URIs' do
232238
conn = stub_connection do |stub|
233239
stub.post('/prediction/v1.2/training?data=12345') do |env|
240+
[200, {}, '{}']
234241
end
235242
end
236243
request = CLIENT.execute(
@@ -244,6 +251,7 @@
244251
it 'should generate requests against the correct URIs' do
245252
conn = stub_connection do |stub|
246253
stub.post('/prediction/v1.2/training?data=12345') do |env|
254+
[200, {}, '{}']
247255
end
248256
end
249257
request = CLIENT.execute(
@@ -264,6 +272,7 @@
264272
conn = stub_connection do |stub|
265273
stub.post('/prediction/v1.2/training') do |env|
266274
env[:url].host.should == 'testing-domain.example.com'
275+
[200, {}, '{}']
267276
end
268277
end
269278

@@ -284,6 +293,7 @@
284293
stub.post('/prediction/v1.2/training?data=12345') do |env|
285294
env[:request_headers].should have_key('Authorization')
286295
env[:request_headers]['Authorization'].should =~ /^OAuth/
296+
[200, {}, '{}']
287297
end
288298
end
289299

@@ -303,6 +313,7 @@
303313
stub.post('/prediction/v1.2/training?data=12345') do |env|
304314
env[:request_headers].should have_key('Authorization')
305315
env[:request_headers]['Authorization'].should =~ /^Bearer/
316+
[200, {}, '{}']
306317
end
307318
end
308319

@@ -363,6 +374,7 @@
363374
stub.post('/prediction/v1.2/training') do |env|
364375
env[:request_headers].should have_key('Content-Type')
365376
env[:request_headers]['Content-Type'].should == 'application/json'
377+
[200, {}, '{}']
366378
end
367379
end
368380
CLIENT.authorization = :oauth_2
@@ -415,6 +427,7 @@
415427
it 'should generate requests against the correct URIs' do
416428
conn = stub_connection do |stub|
417429
stub.get('/plus/v1/people/107807692475771887386/activities/public') do |env|
430+
[200, {}, '{}']
418431
end
419432
end
420433

@@ -485,6 +498,7 @@
485498
it 'should generate requests against the correct URIs' do
486499
conn = stub_connection do |stub|
487500
stub.get('/adsense/v1.3/adclients') do |env|
501+
[200, {}, '{}']
488502
end
489503
end
490504
request = CLIENT.execute(
@@ -515,6 +529,7 @@
515529
it 'should succeed when validating parameters in a correct call' do
516530
conn = stub_connection do |stub|
517531
stub.get('/adsense/v1.3/reports?dimension=DATE&endDate=2010-01-01&metric=PAGE_VIEWS&startDate=2000-01-01') do |env|
532+
[200, {}, '{}']
518533
end
519534
end
520535
(lambda do
@@ -553,6 +568,7 @@
553568
stub.get('/adsense/v1.3/reports?dimension=DATE&dimension=PRODUCT_CODE'+
554569
'&endDate=2010-01-01&metric=CLICKS&metric=PAGE_VIEWS&'+
555570
'startDate=2000-01-01') do |env|
571+
[200, {}, '{}']
556572
end
557573
end
558574
(lambda do
@@ -593,6 +609,7 @@
593609
'startDate=2000-01-01') do |env|
594610
env.params['dimension'].should include('DATE', 'PRODUCT_CODE')
595611
env.params['metric'].should include('CLICKS', 'PAGE_VIEWS')
612+
[200, {}, '{}']
596613
end
597614
end
598615
request = CLIENT.execute(

spec/google/api_client/service_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
it 'should make a valid call for a method with no parameters' do
5050
conn = stub_connection do |stub|
5151
stub.get('/adsense/v1.3/adclients') do |env|
52+
[200, {}, '{}']
5253
end
5354
end
5455
adsense = Google::APIClient::Service.new(
@@ -69,6 +70,7 @@
6970
it 'should make a valid call for a method with parameters' do
7071
conn = stub_connection do |stub|
7172
stub.get('/adsense/v1.3/adclients/1/adunits') do |env|
73+
[200, {}, '{}']
7274
end
7375
end
7476
adsense = Google::APIClient::Service.new(
@@ -87,6 +89,7 @@
8789
it 'should make a valid call for a deep method' do
8890
conn = stub_connection do |stub|
8991
stub.get('/adsense/v1.3/accounts/1/adclients') do |env|
92+
[200, {}, '{}']
9093
end
9194
end
9295
adsense = Google::APIClient::Service.new(
@@ -147,6 +150,7 @@
147150
conn = stub_connection do |stub|
148151
stub.post('/prediction/v1.5/trainedmodels?project=1') do |env|
149152
env.body.should == '{"id":"1"}'
153+
[200, {}, '{}']
150154
end
151155
end
152156
prediction = Google::APIClient::Service.new(
@@ -167,6 +171,7 @@
167171
conn = stub_connection do |stub|
168172
stub.post('/prediction/v1.5/trainedmodels?project=1') do |env|
169173
env.body.should == '{"id":"1"}'
174+
[200, {}, '{}']
170175
end
171176
end
172177
prediction = Google::APIClient::Service.new(
@@ -224,6 +229,7 @@
224229
conn = stub_connection do |stub|
225230
stub.post('/upload/drive/v1/files?uploadType=multipart') do |env|
226231
env.body.should be_a Faraday::CompositeReadIO
232+
[200, {}, '{}']
227233
end
228234
end
229235
drive = Google::APIClient::Service.new(

0 commit comments

Comments
 (0)