Skip to content

Commit 703c92c

Browse files
committedDec 21, 2015
Handle SSL errors with retries
1 parent 46a8071 commit 703c92c

File tree

2 files changed

+110
-33
lines changed

2 files changed

+110
-33
lines changed
 

‎googleapiclient/http.py

+51-33
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import mimetypes
3737
import os
3838
import random
39+
import ssl
3940
import sys
4041
import time
4142
import uuid
@@ -61,6 +62,46 @@
6162
MAX_URI_LENGTH = 2048
6263

6364

65+
def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
66+
**kwargs):
67+
"""Retries an HTTP request multiple times while handling errors.
68+
69+
If after all retries the request still fails, last error is either returned as
70+
return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
71+
72+
Args:
73+
http: Http object to be used to execute request.
74+
num_retries: Maximum number of retries.
75+
req_type: Type of the request (used for logging retries).
76+
sleep, rand: Functions to sleep for random time between retries.
77+
uri: URI to be requested.
78+
method: HTTP method to be used.
79+
args, kwargs: Additional arguments passed to http.request.
80+
81+
Returns:
82+
resp, content - Response from the http request (may be HTTP 5xx).
83+
"""
84+
resp = None
85+
for retry_num in range(num_retries + 1):
86+
if retry_num > 0:
87+
sleep(rand() * 2**retry_num)
88+
logging.warning(
89+
'Retry #%d for %s: %s %s%s' % (retry_num, req_type, method, uri,
90+
', following status: %d' % resp.status if resp else ''))
91+
92+
try:
93+
resp, content = http.request(uri, method, *args, **kwargs)
94+
except ssl.SSLError:
95+
if retry_num == num_retries:
96+
raise
97+
else:
98+
continue
99+
if resp.status < 500:
100+
break
101+
102+
return resp, content
103+
104+
64105
class MediaUploadProgress(object):
65106
"""Status of a resumable upload."""
66107

@@ -546,16 +587,9 @@ def next_chunk(self, num_retries=0):
546587
}
547588
http = self._request.http
548589

549-
for retry_num in range(num_retries + 1):
550-
if retry_num > 0:
551-
self._sleep(self._rand() * 2**retry_num)
552-
logging.warning(
553-
'Retry #%d for media download: GET %s, following status: %d'
554-
% (retry_num, self._uri, resp.status))
555-
556-
resp, content = http.request(self._uri, headers=headers)
557-
if resp.status < 500:
558-
break
590+
resp, content = _retry_request(
591+
http, num_retries, 'media download', self._sleep, self._rand, self._uri,
592+
'GET', headers=headers)
559593

560594
if resp.status in [200, 206]:
561595
if 'content-location' in resp and resp['content-location'] != self._uri:
@@ -654,7 +688,7 @@ def __init__(self, http, postproc, uri,
654688

655689
# Pull the multipart boundary out of the content-type header.
656690
major, minor, params = mimeparse.parse_mime_type(
657-
headers.get('content-type', 'application/json'))
691+
self.headers.get('content-type', 'application/json'))
658692

659693
# The size of the non-media part of the request.
660694
self.body_size = len(self.body or '')
@@ -716,16 +750,9 @@ def execute(self, http=None, num_retries=0):
716750
self.headers['content-length'] = str(len(self.body))
717751

718752
# Handle retries for server-side errors.
719-
for retry_num in range(num_retries + 1):
720-
if retry_num > 0:
721-
self._sleep(self._rand() * 2**retry_num)
722-
logging.warning('Retry #%d for request: %s %s, following status: %d'
723-
% (retry_num, self.method, self.uri, resp.status))
724-
725-
resp, content = http.request(str(self.uri), method=str(self.method),
726-
body=self.body, headers=self.headers)
727-
if resp.status < 500:
728-
break
753+
resp, content = _retry_request(
754+
http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
755+
method=str(self.method), body=self.body, headers=self.headers)
729756

730757
for callback in self.response_callbacks:
731758
callback(resp)
@@ -799,18 +826,9 @@ def next_chunk(self, http=None, num_retries=0):
799826
start_headers['X-Upload-Content-Length'] = size
800827
start_headers['content-length'] = str(self.body_size)
801828

802-
for retry_num in range(num_retries + 1):
803-
if retry_num > 0:
804-
self._sleep(self._rand() * 2**retry_num)
805-
logging.warning(
806-
'Retry #%d for resumable URI request: %s %s, following status: %d'
807-
% (retry_num, self.method, self.uri, resp.status))
808-
809-
resp, content = http.request(self.uri, method=self.method,
810-
body=self.body,
811-
headers=start_headers)
812-
if resp.status < 500:
813-
break
829+
resp, content = _retry_request(
830+
http, num_retries, 'resumable URI request', self._sleep, self._rand,
831+
self.uri, method=self.method, body=self.body, headers=start_headers)
814832

815833
if resp.status == 200 and 'location' in resp:
816834
self.resumable_uri = resp['location']

‎tests/test_http.py

+59
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import os
3535
import unittest2 as unittest
3636
import random
37+
import ssl
3738
import time
3839

3940
from googleapiclient.discovery import build
@@ -101,6 +102,20 @@ def apply(self, headers):
101102
headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
102103

103104

105+
class HttpMockWithSSLErrors(object):
106+
def __init__(self, num_errors, success_json, success_data):
107+
self.num_errors = num_errors
108+
self.success_json = success_json
109+
self.success_data = success_data
110+
111+
def request(self, *args, **kwargs):
112+
if not self.num_errors:
113+
return httplib2.Response(self.success_json), self.success_data
114+
else:
115+
self.num_errors -= 1
116+
raise ssl.SSLError()
117+
118+
104119
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
105120

106121

@@ -394,6 +409,20 @@ def test_media_io_base_download_handle_4xx(self):
394409

395410
self.assertEqual(self.fd.getvalue(), b'123')
396411

412+
def test_media_io_base_download_retries_ssl_errors(self):
413+
self.request.http = HttpMockWithSSLErrors(
414+
3, {'status': '200', 'content-range': '0-2/3'}, b'123')
415+
416+
download = MediaIoBaseDownload(
417+
fd=self.fd, request=self.request, chunksize=3)
418+
download._sleep = lambda _x: 0 # do nothing
419+
download._rand = lambda: 10
420+
421+
status, done = download.next_chunk(num_retries=3)
422+
423+
self.assertEqual(self.fd.getvalue(), b'123')
424+
self.assertEqual(True, done)
425+
397426
def test_media_io_base_download_retries_5xx(self):
398427
self.request.http = HttpMockSequence([
399428
({'status': '500'}, ''),
@@ -593,6 +622,36 @@ def test_unicode(self):
593622
self.assertEqual(method, http.method)
594623
self.assertEqual(str, type(http.method))
595624

625+
def test_retry_ssl_errors_non_resumable(self):
626+
model = JsonModel()
627+
request = HttpRequest(
628+
HttpMockWithSSLErrors(3, {'status': '200'}, '{"foo": "bar"}'),
629+
model.response,
630+
u'https://www.example.com/json_api_endpoint')
631+
request._sleep = lambda _x: 0 # do nothing
632+
request._rand = lambda: 10
633+
response = request.execute(num_retries=3)
634+
self.assertEqual({u'foo': u'bar'}, response)
635+
636+
def test_retry_ssl_errors_resumable(self):
637+
with open(datafile('small.png'), 'rb') as small_png_file:
638+
small_png_fd = BytesIO(small_png_file.read())
639+
upload = MediaIoBaseUpload(fd=small_png_fd, mimetype='image/png',
640+
chunksize=500, resumable=True)
641+
model = JsonModel()
642+
643+
request = HttpRequest(
644+
HttpMockWithSSLErrors(
645+
3, {'status': '200', 'location': 'location'}, '{"foo": "bar"}'),
646+
model.response,
647+
u'https://www.example.com/file_upload',
648+
method='POST',
649+
resumable=upload)
650+
request._sleep = lambda _x: 0 # do nothing
651+
request._rand = lambda: 10
652+
response = request.execute(num_retries=3)
653+
self.assertEqual({u'foo': u'bar'}, response)
654+
596655
def test_retry(self):
597656
num_retries = 5
598657
resp_seq = [({'status': '500'}, '')] * num_retries

0 commit comments

Comments
 (0)
Please sign in to comment.