Skip to content

Commit 77d0fe3

Browse files
Merge pull request #18 from NYPL/SCC-3775/get-request-retries
add multiple retries for empty sierra responses
2 parents eee8983 + 9ac63b3 commit 77d0fe3

3 files changed

Lines changed: 79 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
# Changelog
2+
## v1.1.0
3+
- Add retries for empty responses in oauth2 client. This was added to address a known quirk in the Sierra API where this response is returned:
4+
```
5+
> GET / HTTP/1.1
6+
> Host: ilsstaff.nypl.org
7+
> User-Agent: curl/7.64.1
8+
> Accept: */*
9+
>
10+
```
211

312
## v1.0.4 - 6/28/23
413
- Enforce Kinesis stream 1000 records/second write limit

src/nypl_py_utils/classes/oauth2_api_client.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
from time import sleep
3+
from requests.models import Response
24
from oauthlib.oauth2 import BackendApplicationClient, TokenExpiredError
35
from requests_oauthlib import OAuth2Session
46
from nypl_py_utils.functions.log_helper import create_log
@@ -7,11 +9,14 @@
79
class Oauth2ApiClient:
810
"""
911
Client for interacting with an Oauth2 authenticated API such as NYPL's
10-
Platform API endpoints
12+
Platform API endpoints. Note with_retries is a boolean flag which
13+
determines if empty get requests will be retried 3 times or until
14+
they are successful. This is to address a known issue with the Sierra
15+
API where empty responses are returned intermittently.
1116
"""
1217

1318
def __init__(self, client_id=None, client_secret=None, base_url=None,
14-
token_url=None):
19+
token_url=None, with_retries=False):
1520
self.client_id = client_id \
1621
or os.environ.get('NYPL_API_CLIENT_ID', None)
1722
self.client_secret = client_secret \
@@ -25,11 +30,30 @@ def __init__(self, client_id=None, client_secret=None, base_url=None,
2530

2631
self.logger = create_log('oauth2_api_client')
2732

33+
self.with_retries = with_retries
34+
2835
def get(self, request_path, **kwargs):
2936
"""
3037
Issue an HTTP GET on the given request_path
3138
"""
32-
return self._do_http_method('GET', request_path, **kwargs)
39+
resp = self._do_http_method('GET', request_path, **kwargs)
40+
if resp.json() is None and self.with_retries is True:
41+
retries = \
42+
kwargs.get('retries', 0) + 1
43+
if retries < 3:
44+
self.logger.warning(
45+
f'Retrying get request due to empty response from\
46+
Oauth2 Client using path: {request_path}. \
47+
Retry #{retries}')
48+
sleep(pow(2, retries - 1))
49+
kwargs['retries'] = retries
50+
resp = self.get(request_path, **kwargs)
51+
else:
52+
resp = Response()
53+
resp.message = 'Oauth2 Client: Request failed after 3 \
54+
empty responses received from Oauth2 Client'
55+
resp.status_code = 500
56+
return resp
3357

3458
def post(self, request_path, json, **kwargs):
3559
"""
@@ -79,7 +103,7 @@ def _do_http_method(self, method, request_path, **kwargs):
79103
from None
80104

81105
self._generate_access_token()
82-
return self._do_http_method(method, request_path, **kwargs)
106+
return self._do_http_method(method, request_path, **kwargs).json()
83107

84108
def _create_oauth_client(self):
85109
"""

tests/test_oauth2_api_client.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
TOKEN_URL = 'https://oauth.example.com/oauth/token'
2121

2222

23+
class MockEmptyResponse:
24+
def __init__(self, empty, status_code=None):
25+
self.status_code = status_code
26+
self.empty = empty
27+
28+
def json(self):
29+
if self.empty:
30+
return None
31+
else:
32+
return 'success'
33+
34+
2335
class TestOauth2ApiClient:
2436

2537
@pytest.fixture
@@ -36,6 +48,15 @@ def test_instance(self, requests_mock):
3648
client_secret='clientsecret'
3749
)
3850

51+
@pytest.fixture
52+
def test_instance_with_retries(self, requests_mock):
53+
return Oauth2ApiClient(base_url=BASE_URL,
54+
token_url=TOKEN_URL,
55+
client_id='clientid',
56+
client_secret='clientsecret',
57+
with_retries=True
58+
)
59+
3960
def test_uses_env_vars(self):
4061
env = {
4162
'NYPL_API_CLIENT_ID': 'env client id',
@@ -136,3 +157,24 @@ def test_token_refresh_failure_raises_error(
136157
test_instance._do_http_method('GET', 'foo')
137158
# Expect 1 initial token fetch, plus 3 retries:
138159
assert len(token_server_post.request_history) == 4
160+
161+
def test_http_retry_fail(self, requests_mock, test_instance_with_retries,
162+
token_server_post, mocker):
163+
mocker.patch.object(test_instance_with_retries, '_do_http_method',
164+
return_value=MockEmptyResponse(empty=True))
165+
get_spy = mocker.spy(test_instance_with_retries, 'get')
166+
resp = test_instance_with_retries.get('spaghetti')
167+
assert get_spy.call_count == 3
168+
assert resp.status_code == 500
169+
170+
def test_http_retry_success(self, requests_mock,
171+
test_instance_with_retries,
172+
token_server_post, mocker):
173+
mocker.patch.object(test_instance_with_retries, '_do_http_method',
174+
side_effect=[MockEmptyResponse(empty=True),
175+
MockEmptyResponse(empty=False,
176+
status_code=200)])
177+
get_spy = mocker.spy(test_instance_with_retries, 'get')
178+
resp = test_instance_with_retries.get('spaghetti')
179+
assert get_spy.call_count == 2
180+
assert resp.json() == 'success'

0 commit comments

Comments
 (0)