diff --git a/shipengine/util/sdk_assertions.py b/shipengine/util/sdk_assertions.py index fa6e83b..0528d27 100644 --- a/shipengine/util/sdk_assertions.py +++ b/shipengine/util/sdk_assertions.py @@ -200,18 +200,28 @@ def check_response_for_errors( # Check if status_code is 429 and raises an error if so. if status_code == 429: + request_id = response_body.get("request_id") or response_headers.get( + 'x-shipengine-requestid' + ) retry_after = response_headers["Retry-After"] + if not isinstance(retry_after, str) or not retry_after.isdigit(): + raise ShipEngineError( + message="Unexpected Retry-After header value.", + error_source=ErrorSource.SHIPENGINE.value, + request_id=request_id, + ) + retry_after = int(retry_after) if retry_after > config.timeout: raise ClientTimeoutError( retry_after=config.timeout, error_source=ErrorSource.SHIPENGINE.value, - request_id=response_body["request_id"], + request_id=request_id, ) else: raise RateLimitExceededError( retry_after=retry_after, error_source=ErrorSource.SHIPENGINE.value, - request_id=response_body["request_id"], + request_id=request_id, ) # Check if the status code is 500 and raises an error if so. diff --git a/tests/services/test_validate_addresses.py b/tests/services/test_validate_addresses.py index 2483700..cc2c4f8 100644 --- a/tests/services/test_validate_addresses.py +++ b/tests/services/test_validate_addresses.py @@ -3,12 +3,51 @@ import unittest import urllib.parse as urlparse +import pytest import responses from shipengine.enums import BaseURL, Endpoints +from shipengine.errors import ShipEngineError, RateLimitExceededError from ..util import stub_shipengine_instance, valid_commercial_address +expected_response_body = [ + { + "status": "verified", + "original_address": { + "name": "ShipEngine", + "phone": "1-123-123-1234", + "company_name": "None", + "address_line1": "3800 N Lamar Blvd", + "address_line2": "ste 220", + "address_line3": "None", + "city_locality": "Austin", + "state_province": "TX", + "postal_code": "78756", + "country_code": "US", + "address_residential_indicator": "unknown", + }, + "matched_address": { + "name": "SHIPENGINE", + "phone": "1-123-123-1234", + "company_name": "None", + "address_line1": "3800 N LAMAR BLVD STE 220", + "address_line2": "", + "address_line3": "None", + "city_locality": "AUSTIN", + "state_province": "TX", + "postal_code": "78756-0003", + "country_code": "US", + "address_residential_indicator": "no", + }, + "messages": [], + } +] + +rate_limit_exceeded_response_body = { + "message": "API rate limit exceeded", +} + class TestValidateAddresses(unittest.TestCase): @responses.activate @@ -23,40 +62,7 @@ def test_validate_addresses(self) -> None: "url": urlparse.urljoin( BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.ADDRESSES_VALIDATE.value ), - "body": json.dumps( - [ - { - "status": "verified", - "original_address": { - "name": "ShipEngine", - "phone": "1-123-123-1234", - "company_name": "None", - "address_line1": "3800 N Lamar Blvd", - "address_line2": "ste 220", - "address_line3": "None", - "city_locality": "Austin", - "state_province": "TX", - "postal_code": "78756", - "country_code": "US", - "address_residential_indicator": "unknown", - }, - "matched_address": { - "name": "SHIPENGINE", - "phone": "1-123-123-1234", - "company_name": "None", - "address_line1": "3800 N LAMAR BLVD STE 220", - "address_line2": "", - "address_line3": "None", - "city_locality": "AUSTIN", - "state_province": "TX", - "postal_code": "78756-0003", - "country_code": "US", - "address_residential_indicator": "no", - }, - "messages": [], - } - ] - ), + "body": json.dumps(expected_response_body), "status": 200, "content_type": "application/json", } @@ -65,3 +71,56 @@ def test_validate_addresses(self) -> None: shipengine = stub_shipengine_instance() result = shipengine.validate_addresses(valid_commercial_address()) self.assertEqual(result[0]["status"], "verified") + + @responses.activate + def test_validate_addresses_when_rate_limit_exceeded_and_invalid_retry_value(self) -> None: + """ + Tests that the validate_addresses method handles unexpected Retry-After + header values upon the rate limit being exceeded. + """ + responses.add( + **{ + "method": responses.POST, + "url": urlparse.urljoin( + BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.ADDRESSES_VALIDATE.value + ), + "body": json.dumps(rate_limit_exceeded_response_body), + "status": 429, + "content_type": "application/json", + "headers": { + "Retry-After": "10.1" + } + } + ) + + shipengine = stub_shipengine_instance() + with pytest.raises(ShipEngineError) as exc_info: + shipengine.validate_addresses(valid_commercial_address()) + + self.assertEqual(exc_info.value.message, 'Unexpected Retry-After header value.') + + @responses.activate + def test_validate_addresses_when_rate_limit_exceeded(self) -> None: + """ + Tests that the validate_addresses method handles the rate limit being exceeded + with the appropriate error. + """ + responses.add( + **{ + "method": responses.POST, + "url": urlparse.urljoin( + BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.ADDRESSES_VALIDATE.value + ), + "body": json.dumps(rate_limit_exceeded_response_body), + "status": 429, + "content_type": "application/json", + "headers": { + "Retry-After": "0", # NOTE: we use 0 here so that the test doesn't block + "x-shipengine-requestid": "56b2b0a3-8df5-4cfe-95f0-d7efefdda2ad" + } + } + ) + + shipengine = stub_shipengine_instance() + with pytest.raises(RateLimitExceededError): + shipengine.validate_addresses(valid_commercial_address())