diff --git a/smartystreets_python_sdk/errors.py b/smartystreets_python_sdk/errors.py index 15ac5c8..e72e2ce 100644 --- a/smartystreets_python_sdk/errors.py +++ b/smartystreets_python_sdk/errors.py @@ -4,20 +4,24 @@ PAYMENT_REQUIRED = "Payment Required: There is no active subscription\ for the account associated with the credentials submitted with the request." -FORBIDDEN = "Because the international service is currently in a limited release phase, only approved accounts" \ - " may access the service." +FORBIDDEN = "Forbidden: The request contained valid data and was understood by the server, but the server is\ + refusing action." + +REQUEST_TIMEOUT = "Request timeout error." REQUEST_ENTITY_TOO_LARGE = "Request Entity Too Large: The request body has exceeded the maximum size." -BAD_REQUEST = "Bad Request (Malformed Payload): A GET request lacked a street field or the request body of a\ +BAD_REQUEST = "Bad Request (Malformed Payload): A GET request lacked a required field or the request body of a\ POST request contained malformed JSON." UNPROCESSABLE_ENTITY = "GET request lacked required fields." -TOO_MANY_REQUESTS = "The rate limit has been exceeded." +TOO_MANY_REQUESTS = "Too Many Requests: The rate limit for your account has been exceeded." INTERNAL_SERVER_ERROR = "Internal Server Error." +BAD_GATEWAY = "Bad Gateway error." + SERVICE_UNAVAILABLE = "Service Unavailable. Try again later." GATEWAY_TIMEOUT = "The upstream data provider did not respond in a timely fashion and the request failed. " \ diff --git a/smartystreets_python_sdk/exceptions.py b/smartystreets_python_sdk/exceptions.py index 434c52a..dd8b8bf 100644 --- a/smartystreets_python_sdk/exceptions.py +++ b/smartystreets_python_sdk/exceptions.py @@ -22,6 +22,14 @@ class BadRequestError(SmartyException): pass +class RequestTimeoutError(SmartyException): + pass + + +class BadGatewayError(SmartyException): + pass + + class UnprocessableEntityError(SmartyException): pass diff --git a/smartystreets_python_sdk/status_code_sender.py b/smartystreets_python_sdk/status_code_sender.py index 3ad8711..3648797 100644 --- a/smartystreets_python_sdk/status_code_sender.py +++ b/smartystreets_python_sdk/status_code_sender.py @@ -18,13 +18,19 @@ def send(self, request): response.error = exceptions.NotModifiedError(errors.NOT_MODIFIED, response.find_header('etag')) elif response.status_code == 429: response.error = self.parse_rate_limit_response(response) - elif response.status_code in [400, 401, 402, 403, 413, 422]: - response.error = messageFrom(response, statuses.get(response.status_code)) - else: - response.error = statuses.get(response.status_code) + elif response.status_code != 200: + response.error = messageFrom(response, fallback_error(response.status_code)) return response +def fallback_error(status_code): + error = statuses.get(status_code) + if error is None: + error = exceptions.SmartyException( + 'The server returned an unexpected HTTP status code: {}'.format(status_code)) + return error + + def messageFrom(response, fallback): message = extract_message(response) if message: @@ -51,10 +57,6 @@ def extract_message(response): return message.rstrip() -def ok(): - return None - - def bad_credentials(): return exceptions.BadCredentialsError(errors.BAD_CREDENTIALS) @@ -67,6 +69,10 @@ def forbidden(): return exceptions.ForbiddenError(errors.FORBIDDEN) +def request_timeout(): + return exceptions.RequestTimeoutError(errors.REQUEST_TIMEOUT) + + def request_entity_too_large(): return exceptions.RequestEntityTooLargeError(errors.REQUEST_ENTITY_TOO_LARGE) @@ -83,6 +89,10 @@ def internal_server_error(): return exceptions.InternalServerError(errors.INTERNAL_SERVER_ERROR) +def bad_gateway(): + return exceptions.BadGatewayError(errors.BAD_GATEWAY) + + def service_unavailable(): return exceptions.ServiceUnavailableError(errors.SERVICE_UNAVAILABLE) @@ -91,14 +101,15 @@ def gateway_timeout(): return exceptions.GatewayTimeoutError(errors.GATEWAY_TIMEOUT) -statuses = {200: ok(), - 401: bad_credentials(), +statuses = {401: bad_credentials(), 402: payment_required(), 403: forbidden(), + 408: request_timeout(), 413: request_entity_too_large(), 400: bad_request(), 422: unprocessable_entity(), 500: internal_server_error(), + 502: bad_gateway(), 503: service_unavailable(), 504: gateway_timeout() } diff --git a/test/status_code_sender_test.py b/test/status_code_sender_test.py index 26c4279..9d52751 100644 --- a/test/status_code_sender_test.py +++ b/test/status_code_sender_test.py @@ -1,6 +1,6 @@ import unittest -from smartystreets_python_sdk import Response, errors +from smartystreets_python_sdk import Response, errors, exceptions from smartystreets_python_sdk.exceptions import BadRequestError, NotModifiedError from smartystreets_python_sdk.status_code_sender import StatusCodeSender from test.mocks import MockSender @@ -70,6 +70,79 @@ def test_client_error_falls_back_when_no_message(self): self.assertIsInstance(response.error, BadRequestError) self.assertEqual(errors.BAD_REQUEST, str(response.error)) + def test_each_status_code_uses_api_message_when_present(self): + payload = '{"errors": [{"message": "API says no"}]}' + expected_types = {400: exceptions.BadRequestError, + 401: exceptions.BadCredentialsError, + 402: exceptions.PaymentRequiredError, + 403: exceptions.ForbiddenError, + 408: exceptions.RequestTimeoutError, + 413: exceptions.RequestEntityTooLargeError, + 422: exceptions.UnprocessableEntityError, + 429: exceptions.TooManyRequestsError, + 500: exceptions.InternalServerError, + 502: exceptions.BadGatewayError, + 503: exceptions.ServiceUnavailableError, + 504: exceptions.GatewayTimeoutError} + + for status_code, error_type in expected_types.items(): + with self.subTest(status_code=status_code): + inner = MockSender(Response(payload, status_code, {})) + + response = StatusCodeSender(inner).send(None) + + self.assertIsInstance(response.error, error_type) + self.assertEqual('API says no', str(response.error)) + + def test_each_status_code_falls_back_to_standard_message(self): + expected_messages = {400: errors.BAD_REQUEST, + 401: errors.BAD_CREDENTIALS, + 402: errors.PAYMENT_REQUIRED, + 403: errors.FORBIDDEN, + 408: errors.REQUEST_TIMEOUT, + 413: errors.REQUEST_ENTITY_TOO_LARGE, + 422: errors.UNPROCESSABLE_ENTITY, + 429: errors.TOO_MANY_REQUESTS, + 500: errors.INTERNAL_SERVER_ERROR, + 502: errors.BAD_GATEWAY, + 503: errors.SERVICE_UNAVAILABLE, + 504: errors.GATEWAY_TIMEOUT} + + for status_code, message in expected_messages.items(): + with self.subTest(status_code=status_code): + inner = MockSender(Response("", status_code, {})) + + response = StatusCodeSender(inner).send(None) + + self.assertEqual(message, str(response.error)) + + def test_standard_messages_match_shared_wording(self): + self.assertEqual("Bad Request (Malformed Payload): A GET request lacked a required field or the" + " request body of a POST request contained malformed JSON.", errors.BAD_REQUEST) + self.assertEqual("Forbidden: The request contained valid data and was understood by the server," + " but the server is refusing action.", errors.FORBIDDEN) + self.assertEqual("Request timeout error.", errors.REQUEST_TIMEOUT) + self.assertEqual("Too Many Requests: The rate limit for your account has been exceeded.", + errors.TOO_MANY_REQUESTS) + self.assertEqual("Bad Gateway error.", errors.BAD_GATEWAY) + + def test_unexpected_status_code_falls_back_to_standard_message(self): + inner = MockSender(Response("", 418, {})) + + response = StatusCodeSender(inner).send(None) + + self.assertIsInstance(response.error, exceptions.SmartyException) + self.assertEqual('The server returned an unexpected HTTP status code: 418', str(response.error)) + + def test_unexpected_status_code_uses_api_message_when_present(self): + payload = '{"errors": [{"message": "API teapot message"}]}' + inner = MockSender(Response(payload, 418, {})) + + response = StatusCodeSender(inner).send(None) + + self.assertIsInstance(response.error, exceptions.SmartyException) + self.assertEqual('API teapot message', str(response.error)) + if __name__ == '__main__': unittest.main()