Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions googleapiclient/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ def _should_retry_response(resp_status, content):
if resp_status == _TOO_MANY_REQUESTS:
return True

# For 403 errors, we have to check for the `reason` in the response to
# For 403 and 400 errors, we have to check for the `reason` in the response to
# determine if we should retry.
if resp_status == http_client.FORBIDDEN:
# If there's no details about the 403 type, don't retry.
if resp_status in [http_client.FORBIDDEN, http_client.BAD_REQUEST]:
# If there's no details about the error, don't retry.
if not content:
return False

Expand Down Expand Up @@ -137,11 +137,16 @@ def _should_retry_response(resp_status, content):
LOGGER.warning("Invalid JSON content from response: %s", content)
return False

LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)

# Only retry on rate limit related failures.
if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
return True
if resp_status == http_client.FORBIDDEN:
LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
# Only retry on rate limit related failures.
if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
return True
elif resp_status == http_client.BAD_REQUEST:
LOGGER.warning('Encountered 400 Bad Request with reason "%s"', reason)
# Only retry on precondition failures.
if reason in ("failedPrecondition", "preconditionFailed"):
return True
Comment on lines +140 to +149
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if/elif block for handling retryable reasons can be refactored to be more data-driven. Using a dictionary to map status codes to their retryable reasons would make this logic more concise and easier to extend with new status codes or reasons in the future.

Suggested change
if resp_status == http_client.FORBIDDEN:
LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
# Only retry on rate limit related failures.
if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
return True
elif resp_status == http_client.BAD_REQUEST:
LOGGER.warning('Encountered 400 Bad Request with reason "%s"', reason)
# Only retry on precondition failures.
if reason in ("failedPrecondition", "preconditionFailed"):
return True
RETRYABLE_REASONS = {
http_client.FORBIDDEN: (
{'userRateLimitExceeded', 'rateLimitExceeded'},
'Encountered 403 Forbidden with reason "%s"',
),
http_client.BAD_REQUEST: (
{'failedPrecondition', 'preconditionFailed'},
'Encountered 400 Bad Request with reason "%s"',
),
}
if resp_status in RETRYABLE_REASONS:
reasons, log_message = RETRYABLE_REASONS[resp_status]
if reason in reasons:
LOGGER.warning(log_message, reason)
return True


# Everything else is a success or non-retriable so break.
return False
Expand Down
42 changes: 42 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,20 @@ def test_media_io_base_download_unknown_media_size(self):
}
}"""

FAILED_PRECONDITION_RESPONSE = """{
"error": {
"errors": [
{
"domain": "global",
"reason": "failedPrecondition",
"message": "Precondition Failed"
}
],
"code": 400,
"message": "Precondition Failed"
}
}"""


NOT_CONFIGURED_RESPONSE = """{
"error": {
Expand Down Expand Up @@ -1056,6 +1070,34 @@ def test_no_retry_succeeds(self):

self.assertEqual(0, len(sleeptimes))

def test_retry_400_failed_precondition(self):
num_retries = 2
resp_seq = [
({"status": "400"}, FAILED_PRECONDITION_RESPONSE),
({"status": "200"}, "{}")
]
http = HttpMockSequence(resp_seq)
model = JsonModel()
uri = "https://www.googleapis.com/someapi/v1/collection/?foo=bar"
method = "POST"
request = HttpRequest(
http,
model.response,
uri,
method=method,
body="{}",
headers={"content-type": "application/json"},
)

sleeptimes = []
request._sleep = lambda x: sleeptimes.append(x)
request._rand = lambda: 10

request.execute(num_retries=num_retries)

self.assertEqual(1, len(sleeptimes))
self.assertEqual(10 * 2 ** 1, sleeptimes[0])

def test_no_retry_fails_fast(self):
http = HttpMockSequence([({"status": "500"}, ""), ({"status": "200"}, "{}")])
model = JsonModel()
Expand Down
Loading