Skip to content

Commit f337b94

Browse files
authored
feat: support auto-retries of 5xx HTTP responses (#78)
2 parents 18369e5 + 60361b6 commit f337b94

File tree

4 files changed

+175
-4
lines changed

4 files changed

+175
-4
lines changed

openfga_sdk/api_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ApiValueError,
3131
FgaValidationException,
3232
RateLimitExceededError,
33+
ServiceException,
3334
)
3435

3536
DEFAULT_USER_AGENT = "openfga-sdk python/0.4.1"
@@ -257,8 +258,8 @@ async def __call_api(
257258
_preload_content=_preload_content,
258259
_request_timeout=_request_timeout,
259260
)
260-
except RateLimitExceededError as e:
261-
if x < max_retry:
261+
except (RateLimitExceededError, ServiceException) as e:
262+
if x < max_retry and e.status != 501:
262263
await asyncio.sleep(random_time(x, min_wait_in_ms))
263264

264265
continue

openfga_sdk/sync/api_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ApiValueError,
3030
FgaValidationException,
3131
RateLimitExceededError,
32+
ServiceException,
3233
)
3334
from openfga_sdk.sync import oauth2, rest
3435

@@ -256,8 +257,8 @@ def __call_api(
256257
_preload_content=_preload_content,
257258
_request_timeout=_request_timeout,
258259
)
259-
except RateLimitExceededError as e:
260-
if x < max_retry:
260+
except (RateLimitExceededError, ServiceException) as e:
261+
if x < max_retry and e.status != 501:
261262
time.sleep(random_time(x, min_wait_in_ms))
262263
continue
263264
e.body = e.body.decode("utf-8")

test/test_open_fga_api.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,8 +1250,10 @@ async def test_500_error(self, mock_request):
12501250
http_resp=http_mock_response(response_body, 500)
12511251
)
12521252

1253+
retry = openfga_sdk.configuration.RetryParams(0, 10)
12531254
configuration = self.configuration
12541255
configuration.store_id = store_id
1256+
configuration.retry_params = retry
12551257
async with openfga_sdk.ApiClient(configuration) as api_client:
12561258
api_instance = open_fga_api.OpenFgaApi(api_client)
12571259
body = CheckRequest(
@@ -1276,6 +1278,89 @@ async def test_500_error(self, mock_request):
12761278
api_exception.exception.parsed_exception.message,
12771279
"Internal Server Error",
12781280
)
1281+
mock_request.assert_called()
1282+
self.assertEqual(mock_request.call_count, 1)
1283+
1284+
@patch.object(rest.RESTClientObject, "request")
1285+
async def test_500_error_retry(self, mock_request):
1286+
"""
1287+
Test to ensure 5xxx retries are handled properly
1288+
"""
1289+
response_body = """
1290+
{
1291+
"code": "internal_error",
1292+
"message": "Internal Server Error"
1293+
}
1294+
"""
1295+
mock_request.side_effect = [
1296+
ServiceException(http_resp=http_mock_response(response_body, 500)),
1297+
ServiceException(http_resp=http_mock_response(response_body, 502)),
1298+
ServiceException(http_resp=http_mock_response(response_body, 503)),
1299+
ServiceException(http_resp=http_mock_response(response_body, 504)),
1300+
mock_response(response_body, 200),
1301+
]
1302+
1303+
retry = openfga_sdk.configuration.RetryParams(5, 10)
1304+
configuration = self.configuration
1305+
configuration.store_id = store_id
1306+
configuration.retry_params = retry
1307+
1308+
async with openfga_sdk.ApiClient(configuration) as api_client:
1309+
api_instance = open_fga_api.OpenFgaApi(api_client)
1310+
body = CheckRequest(
1311+
tuple_key=TupleKey(
1312+
object="document:2021-budget",
1313+
relation="reader",
1314+
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
1315+
),
1316+
)
1317+
1318+
api_response = await api_instance.check(
1319+
body=body,
1320+
)
1321+
1322+
self.assertIsInstance(api_response, CheckResponse)
1323+
mock_request.assert_called()
1324+
self.assertEqual(mock_request.call_count, 5)
1325+
1326+
@patch.object(rest.RESTClientObject, "request")
1327+
async def test_501_error_retry(self, mock_request):
1328+
"""
1329+
Test to ensure 501 responses are not auto-retried
1330+
"""
1331+
response_body = """
1332+
{
1333+
"code": "not_implemented",
1334+
"message": "Not Implemented"
1335+
}
1336+
"""
1337+
mock_request.side_effect = [
1338+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1339+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1340+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1341+
mock_response(response_body, 200),
1342+
]
1343+
1344+
retry = openfga_sdk.configuration.RetryParams(5, 10)
1345+
configuration = self.configuration
1346+
configuration.store_id = store_id
1347+
configuration.retry_params = retry
1348+
1349+
async with openfga_sdk.ApiClient(configuration) as api_client:
1350+
api_instance = open_fga_api.OpenFgaApi(api_client)
1351+
body = CheckRequest(
1352+
tuple_key=TupleKey(
1353+
object="document:2021-budget",
1354+
relation="reader",
1355+
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
1356+
),
1357+
)
1358+
with self.assertRaises(ServiceException) as api_exception:
1359+
await api_instance.check(
1360+
body=body,
1361+
)
1362+
mock_request.assert_called()
1363+
self.assertEqual(mock_request.call_count, 1)
12791364

12801365
@patch.object(rest.RESTClientObject, "request")
12811366
async def test_check_api_token(self, mock_request):

test/test_open_fga_api_sync.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,8 +1247,11 @@ async def test_500_error(self, mock_request):
12471247
http_resp=http_mock_response(response_body, 500)
12481248
)
12491249

1250+
retry = openfga_sdk.configuration.RetryParams(0, 10)
12501251
configuration = self.configuration
12511252
configuration.store_id = store_id
1253+
configuration.retry_params = retry
1254+
12521255
with ApiClient(configuration) as api_client:
12531256
api_instance = open_fga_api.OpenFgaApi(api_client)
12541257
body = CheckRequest(
@@ -1274,6 +1277,87 @@ async def test_500_error(self, mock_request):
12741277
"Internal Server Error",
12751278
)
12761279

1280+
@patch.object(rest.RESTClientObject, "request")
1281+
async def test_500_error(self, mock_request):
1282+
"""
1283+
Test to ensure 5xx retries are handled properly
1284+
"""
1285+
response_body = """
1286+
{
1287+
"code": "internal_error",
1288+
"message": "Internal Server Error"
1289+
}
1290+
"""
1291+
mock_request.side_effect = [
1292+
ServiceException(http_resp=http_mock_response(response_body, 500)),
1293+
ServiceException(http_resp=http_mock_response(response_body, 502)),
1294+
ServiceException(http_resp=http_mock_response(response_body, 503)),
1295+
ServiceException(http_resp=http_mock_response(response_body, 504)),
1296+
mock_response(response_body, 200),
1297+
]
1298+
1299+
retry = openfga_sdk.configuration.RetryParams(5, 10)
1300+
configuration = self.configuration
1301+
configuration.store_id = store_id
1302+
configuration.retry_params = retry
1303+
1304+
with ApiClient(configuration) as api_client:
1305+
api_instance = open_fga_api.OpenFgaApi(api_client)
1306+
body = CheckRequest(
1307+
tuple_key=TupleKey(
1308+
object="document:2021-budget",
1309+
relation="reader",
1310+
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
1311+
),
1312+
)
1313+
1314+
api_response = api_instance.check(
1315+
body=body,
1316+
)
1317+
1318+
self.assertIsInstance(api_response, CheckResponse)
1319+
mock_request.assert_called()
1320+
self.assertEqual(mock_request.call_count, 5)
1321+
1322+
@patch.object(rest.RESTClientObject, "request")
1323+
async def test_501_error_retry(self, mock_request):
1324+
"""
1325+
Test to ensure 501 responses are not auto-retried
1326+
"""
1327+
response_body = """
1328+
{
1329+
"code": "not_implemented",
1330+
"message": "Not Implemented"
1331+
}
1332+
"""
1333+
mock_request.side_effect = [
1334+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1335+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1336+
ServiceException(http_resp=http_mock_response(response_body, 501)),
1337+
mock_response(response_body, 200),
1338+
]
1339+
1340+
retry = openfga_sdk.configuration.RetryParams(5, 10)
1341+
configuration = self.configuration
1342+
configuration.store_id = store_id
1343+
configuration.retry_params = retry
1344+
1345+
with ApiClient(configuration) as api_client:
1346+
api_instance = open_fga_api.OpenFgaApi(api_client)
1347+
body = CheckRequest(
1348+
tuple_key=TupleKey(
1349+
object="document:2021-budget",
1350+
relation="reader",
1351+
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
1352+
),
1353+
)
1354+
with self.assertRaises(ServiceException) as api_exception:
1355+
api_instance.check(
1356+
body=body,
1357+
)
1358+
mock_request.assert_called()
1359+
self.assertEqual(mock_request.call_count, 1)
1360+
12771361
@patch.object(rest.RESTClientObject, "request")
12781362
async def test_check_api_token(self, mock_request):
12791363
"""Test case for API token

0 commit comments

Comments
 (0)