From 31ad7f5ad0781bdbaf6a65795054dd679abf454e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 3 Jul 2021 20:14:27 -0700 Subject: [PATCH 1/7] feat(data-classes): add common service errors Changes: - Add base ServiceError exception for rest api service errors - BaseRequestError for 400s - UnauthorizedError for 401s - NotFoundError for 404s - InternalServerError for 500s --- .../event_handler/api_gateway.py | 89 ++++++++++++++++++- .../event_handler/test_api_gateway.py | 51 ++++++++++- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 2b1e1fc0900..89de0388e91 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -12,6 +12,79 @@ from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) +APPLICATION_JSON = "application/json" + + +class ServiceError(Exception): + """Service Error""" + + def __init__(self, status_code: int, message: str): + """ + Parameters + ---------- + code: int + Http status code + message: str + Error message + """ + self.status_code = status_code + self.message = message + + def __str__(self) -> str: + """To string of the message only""" + return self.message + + +class BadRequestError(ServiceError): + """Bad Request Error""" + + def __init__(self, message: str): + """ + Parameters + ---------- + message: str + Error message + """ + super().__init__(400, message) + + +class UnauthorizedError(ServiceError): + """Unauthorized Error""" + + def __init__(self, message: str): + """ + Parameters + ---------- + message: str + Error message + """ + super().__init__(401, message) + + +class NotFoundError(ServiceError): + """Not Found Error""" + + def __init__(self, message: str = "Not found"): + """ + Parameters + ---------- + message: str + Error message + """ + super().__init__(404, message) + + +class InternalServerError(ServiceError): + """Internal Serve Error""" + + def __init__(self, message: str): + """ + Parameters + ---------- + message: str + Error message + """ + super().__init__(500, message) class ProxyEventType(Enum): @@ -467,7 +540,7 @@ def _not_found(self, method: str) -> ResponseBuilder: return ResponseBuilder( Response( status_code=404, - content_type="application/json", + content_type=APPLICATION_JSON, headers=headers, body=json.dumps({"message": "Not found"}), ) @@ -475,7 +548,17 @@ def _not_found(self, method: str) -> ResponseBuilder: def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: """Actually call the matching route with any provided keyword arguments.""" - return ResponseBuilder(self._to_response(route.func(**args)), route) + try: + return ResponseBuilder(self._to_response(route.func(**args)), route) + except ServiceError as e: + return ResponseBuilder( + Response( + status_code=e.status_code, + content_type=APPLICATION_JSON, + body=json.dumps({"message": str(e)}), + ), + route, + ) @staticmethod def _to_response(result: Union[Dict, Response]) -> Response: @@ -493,6 +576,6 @@ def _to_response(result: Union[Dict, Response]) -> Response: logger.debug("Simple response detected, serializing return before constructing final response") return Response( status_code=200, - content_type="application/json", + content_type=APPLICATION_JSON, body=json.dumps(result, separators=(",", ":"), cls=Encoder), ) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index caaaeb1b97b..3e086bb7c77 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -6,11 +6,15 @@ from typing import Dict from aws_lambda_powertools.event_handler.api_gateway import ( + APPLICATION_JSON, ApiGatewayResolver, + BadRequestError, CORSConfig, + NotFoundError, ProxyEventType, Response, ResponseBuilder, + ServiceError, ) from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 @@ -24,7 +28,6 @@ def read_media(file_name: str) -> bytes: LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json") TEXT_HTML = "text/html" -APPLICATION_JSON = "application/json" def test_alb_event(): @@ -429,6 +432,7 @@ def test_no_matches_with_cors(): # AND cors headers are returned assert result["statusCode"] == 404 assert "Access-Control-Allow-Origin" in result["headers"] + assert "Not found" in result["body"] def test_cors_preflight(): @@ -490,3 +494,48 @@ def custom_method(): assert headers["Content-Type"] == TEXT_HTML assert "Access-Control-Allow-Origin" in result["headers"] assert headers["Access-Control-Allow-Methods"] == "CUSTOM" + + +def test_service_error_response(): + # GIVEN a service error response + app = ApiGatewayResolver(cors=CORSConfig()) + + @app.route(method="GET", rule="/service-error", cors=True) + def service_error(): + raise ServiceError(403, "Unauthorized") + + @app.route(method="GET", rule="/not-found-error", cors=False) + def not_found_error(): + raise NotFoundError + + @app.route(method="GET", rule="/bad-request-error", cors=False) + def bad_request_error(): + raise BadRequestError("Missing required parameter") + + # WHEN calling the handler + # AND path is /service-error + result = app({"path": "/service-error", "httpMethod": "GET"}, None) + # THEN return the service error response + # AND status code equals 403 + assert result["statusCode"] == 403 + assert result["body"] == json.dumps({"message": "Unauthorized"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert "Access-Control-Allow-Origin" in result["headers"] + + # WHEN calling the handler + # AND path is /not-found-error + result = app({"path": "/not-found-error", "httpMethod": "GET"}, None) + # THEN return the not found error response + # AND status code equals 404 + assert result["statusCode"] == 404 + assert result["body"] == json.dumps({"message": "Not found"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON + + # WHEN calling the handler + # AND path is /bad-request-error + result = app({"path": "/bad-request-error", "httpMethod": "GET"}, None) + # THEN return the bad request error response + # AND status code equals 400 + assert result["statusCode"] == 400 + assert result["body"] == json.dumps({"message": "Missing required parameter"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON From 6e12e3099cb48e85ab4b8c75745d604861a6d553 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 3 Jul 2021 20:25:39 -0700 Subject: [PATCH 2/7] test(data-classes): add missing test --- tests/functional/event_handler/test_api_gateway.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 3e086bb7c77..5d1983df7d0 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -15,6 +15,7 @@ Response, ResponseBuilder, ServiceError, + UnauthorizedError, ) from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 @@ -512,6 +513,10 @@ def not_found_error(): def bad_request_error(): raise BadRequestError("Missing required parameter") + @app.route(method="GET", rule="/unauthorized-error", cors=False) + def unauthorized_error(): + raise UnauthorizedError("Unauthorized") + # WHEN calling the handler # AND path is /service-error result = app({"path": "/service-error", "httpMethod": "GET"}, None) @@ -539,3 +544,12 @@ def bad_request_error(): assert result["statusCode"] == 400 assert result["body"] == json.dumps({"message": "Missing required parameter"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON + + # WHEN calling the handler + # AND path is /unauthorized-error + result = app({"path": "/unauthorized-error", "httpMethod": "GET"}, None) + # THEN return the unauthorized error response + # AND status code equals 401 + assert result["statusCode"] == 401 + assert result["body"] == json.dumps({"message": "Unauthorized"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON From 88726a445cadf85b138561d550341e0b21e7eefd Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 3 Jul 2021 20:35:30 -0700 Subject: [PATCH 3/7] test(data-classes): add missing test --- .../event_handler/test_api_gateway.py | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 5d1983df7d0..84b2bc6fd9a 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -10,6 +10,7 @@ ApiGatewayResolver, BadRequestError, CORSConfig, + InternalServerError, NotFoundError, ProxyEventType, Response, @@ -501,6 +502,14 @@ def test_service_error_response(): # GIVEN a service error response app = ApiGatewayResolver(cors=CORSConfig()) + @app.route(method="GET", rule="/bad-request-error", cors=False) + def bad_request_error(): + raise BadRequestError("Missing required parameter") + + @app.route(method="GET", rule="/unauthorized-error", cors=False) + def unauthorized_error(): + raise UnauthorizedError("Unauthorized") + @app.route(method="GET", rule="/service-error", cors=True) def service_error(): raise ServiceError(403, "Unauthorized") @@ -509,13 +518,27 @@ def service_error(): def not_found_error(): raise NotFoundError - @app.route(method="GET", rule="/bad-request-error", cors=False) - def bad_request_error(): - raise BadRequestError("Missing required parameter") + @app.route(method="GET", rule="/internal-server-error", cors=False) + def internal_server_error(): + raise InternalServerError("Internal server error") - @app.route(method="GET", rule="/unauthorized-error", cors=False) - def unauthorized_error(): - raise UnauthorizedError("Unauthorized") + # WHEN calling the handler + # AND path is /bad-request-error + result = app({"path": "/bad-request-error", "httpMethod": "GET"}, None) + # THEN return the bad request error response + # AND status code equals 400 + assert result["statusCode"] == 400 + assert result["body"] == json.dumps({"message": "Missing required parameter"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON + + # WHEN calling the handler + # AND path is /unauthorized-error + result = app({"path": "/unauthorized-error", "httpMethod": "GET"}, None) + # THEN return the unauthorized error response + # AND status code equals 401 + assert result["statusCode"] == 401 + assert result["body"] == json.dumps({"message": "Unauthorized"}) + assert result["headers"]["Content-Type"] == APPLICATION_JSON # WHEN calling the handler # AND path is /service-error @@ -537,19 +560,10 @@ def unauthorized_error(): assert result["headers"]["Content-Type"] == APPLICATION_JSON # WHEN calling the handler - # AND path is /bad-request-error - result = app({"path": "/bad-request-error", "httpMethod": "GET"}, None) - # THEN return the bad request error response - # AND status code equals 400 - assert result["statusCode"] == 400 - assert result["body"] == json.dumps({"message": "Missing required parameter"}) - assert result["headers"]["Content-Type"] == APPLICATION_JSON - - # WHEN calling the handler - # AND path is /unauthorized-error - result = app({"path": "/unauthorized-error", "httpMethod": "GET"}, None) - # THEN return the unauthorized error response - # AND status code equals 401 - assert result["statusCode"] == 401 - assert result["body"] == json.dumps({"message": "Unauthorized"}) + # AND path is /internal-server-error + result = app({"path": "/internal-server-error", "httpMethod": "GET"}, None) + # THEN return the internal server error response + # AND status code equals 500 + assert result["statusCode"] == 500 + assert result["body"] == json.dumps({"message": "Internal server error"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON From 4d37480c4162fbacd3e73b7f6a535f3b2671050f Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 4 Jul 2021 20:04:28 -0700 Subject: [PATCH 4/7] refactor: add status to the response --- .../event_handler/api_gateway.py | 64 +++++---------- .../event_handler/test_api_gateway.py | 77 +++++++++++-------- 2 files changed, 65 insertions(+), 76 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 89de0388e91..65c7fd5d72f 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -18,72 +18,44 @@ class ServiceError(Exception): """Service Error""" - def __init__(self, status_code: int, message: str): + def __init__(self, status_code: int, msg: str): """ Parameters ---------- - code: int + status_code: int Http status code - message: str + msg: str Error message """ self.status_code = status_code - self.message = message - - def __str__(self) -> str: - """To string of the message only""" - return self.message + self.msg = msg class BadRequestError(ServiceError): """Bad Request Error""" - def __init__(self, message: str): - """ - Parameters - ---------- - message: str - Error message - """ - super().__init__(400, message) + def __init__(self, msg: str): + super().__init__(400, msg) class UnauthorizedError(ServiceError): """Unauthorized Error""" - def __init__(self, message: str): - """ - Parameters - ---------- - message: str - Error message - """ - super().__init__(401, message) + def __init__(self, msg: str): + super().__init__(401, msg) class NotFoundError(ServiceError): """Not Found Error""" - def __init__(self, message: str = "Not found"): - """ - Parameters - ---------- - message: str - Error message - """ - super().__init__(404, message) + def __init__(self, msg: str = "Not found"): + super().__init__(404, msg) class InternalServerError(ServiceError): - """Internal Serve Error""" + """Internal Server Error""" def __init__(self, message: str): - """ - Parameters - ---------- - message: str - Error message - """ super().__init__(500, message) @@ -542,7 +514,7 @@ def _not_found(self, method: str) -> ResponseBuilder: status_code=404, content_type=APPLICATION_JSON, headers=headers, - body=json.dumps({"message": "Not found"}), + body=self._json_dump({"code": 404, "message": "Not found"}), ) ) @@ -555,13 +527,12 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: Response( status_code=e.status_code, content_type=APPLICATION_JSON, - body=json.dumps({"message": str(e)}), + body=self._json_dump({"code": e.status_code, "message": e.msg}), ), route, ) - @staticmethod - def _to_response(result: Union[Dict, Response]) -> Response: + def _to_response(self, result: Union[Dict, Response]) -> Response: """Convert the route's result to a Response 2 main result types are supported: @@ -577,5 +548,10 @@ def _to_response(result: Union[Dict, Response]) -> Response: return Response( status_code=200, content_type=APPLICATION_JSON, - body=json.dumps(result, separators=(",", ":"), cls=Encoder), + body=self._json_dump(result), ) + + @staticmethod + def _json_dump(obj: Any) -> str: + """Does a concise json serialization""" + return json.dumps(obj, separators=(",", ":"), cls=Encoder) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 84b2bc6fd9a..7ece2579bb8 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -498,38 +498,32 @@ def custom_method(): assert headers["Access-Control-Allow-Methods"] == "CUSTOM" -def test_service_error_response(): - # GIVEN a service error response +def test_service_error_responses(): + # SCENARIO handling different kind of service errors being raised app = ApiGatewayResolver(cors=CORSConfig()) - @app.route(method="GET", rule="/bad-request-error", cors=False) + def json_dump(obj): + return json.dumps(obj, separators=(",", ":")) + + # GIVEN an BadRequestError + @app.get(rule="/bad-request-error", cors=False) def bad_request_error(): raise BadRequestError("Missing required parameter") - @app.route(method="GET", rule="/unauthorized-error", cors=False) - def unauthorized_error(): - raise UnauthorizedError("Unauthorized") - - @app.route(method="GET", rule="/service-error", cors=True) - def service_error(): - raise ServiceError(403, "Unauthorized") - - @app.route(method="GET", rule="/not-found-error", cors=False) - def not_found_error(): - raise NotFoundError - - @app.route(method="GET", rule="/internal-server-error", cors=False) - def internal_server_error(): - raise InternalServerError("Internal server error") - # WHEN calling the handler # AND path is /bad-request-error result = app({"path": "/bad-request-error", "httpMethod": "GET"}, None) # THEN return the bad request error response # AND status code equals 400 assert result["statusCode"] == 400 - assert result["body"] == json.dumps({"message": "Missing required parameter"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON + expected = {"code": 400, "message": "Missing required parameter"} + assert result["body"] == json_dump(expected) + + # GIVEN an UnauthorizedError + @app.get(rule="/unauthorized-error", cors=False) + def unauthorized_error(): + raise UnauthorizedError("Unauthorized") # WHEN calling the handler # AND path is /unauthorized-error @@ -537,18 +531,14 @@ def internal_server_error(): # THEN return the unauthorized error response # AND status code equals 401 assert result["statusCode"] == 401 - assert result["body"] == json.dumps({"message": "Unauthorized"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON + expected = {"code": 401, "message": "Unauthorized"} + assert result["body"] == json_dump(expected) - # WHEN calling the handler - # AND path is /service-error - result = app({"path": "/service-error", "httpMethod": "GET"}, None) - # THEN return the service error response - # AND status code equals 403 - assert result["statusCode"] == 403 - assert result["body"] == json.dumps({"message": "Unauthorized"}) - assert result["headers"]["Content-Type"] == APPLICATION_JSON - assert "Access-Control-Allow-Origin" in result["headers"] + # GIVEN an NotFoundError + @app.get(rule="/not-found-error", cors=False) + def not_found_error(): + raise NotFoundError # WHEN calling the handler # AND path is /not-found-error @@ -556,8 +546,14 @@ def internal_server_error(): # THEN return the not found error response # AND status code equals 404 assert result["statusCode"] == 404 - assert result["body"] == json.dumps({"message": "Not found"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON + expected = {"code": 404, "message": "Not found"} + assert result["body"] == json_dump(expected) + + # GIVEN an InternalServerError + @app.get(rule="/internal-server-error", cors=False) + def internal_server_error(): + raise InternalServerError("Internal server error") # WHEN calling the handler # AND path is /internal-server-error @@ -565,5 +561,22 @@ def internal_server_error(): # THEN return the internal server error response # AND status code equals 500 assert result["statusCode"] == 500 - assert result["body"] == json.dumps({"message": "Internal server error"}) assert result["headers"]["Content-Type"] == APPLICATION_JSON + expected = {"code": 500, "message": "Internal server error"} + assert result["body"] == json_dump(expected) + + # GIVEN an ServiceError with a custom status code + @app.get(rule="/service-error", cors=True) + def service_error(): + raise ServiceError(502, "Something went wrong!") + + # WHEN calling the handler + # AND path is /service-error + result = app({"path": "/service-error", "httpMethod": "GET"}, None) + # THEN return the service error response + # AND status code equals 502 + assert result["statusCode"] == 502 + assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert "Access-Control-Allow-Origin" in result["headers"] + expected = {"code": 502, "message": "Something went wrong!"} + assert result["body"] == json_dump(expected) From e6154317b2bbcbced6723dae033462ca65a4852a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 5 Jul 2021 12:21:00 -0700 Subject: [PATCH 5/7] refactor(api-gateway): rename to statusCode --- .../event_handler/api_gateway.py | 15 ++++++++------- .../functional/event_handler/test_api_gateway.py | 10 +++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 65c7fd5d72f..464d80bc58a 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -4,6 +4,7 @@ import re import zlib from enum import Enum +from http import HTTPStatus from typing import Any, Callable, Dict, List, Optional, Set, Union from aws_lambda_powertools.shared.json_encoder import Encoder @@ -35,28 +36,28 @@ class BadRequestError(ServiceError): """Bad Request Error""" def __init__(self, msg: str): - super().__init__(400, msg) + super().__init__(HTTPStatus.BAD_REQUEST.value, msg) class UnauthorizedError(ServiceError): """Unauthorized Error""" def __init__(self, msg: str): - super().__init__(401, msg) + super().__init__(HTTPStatus.UNAUTHORIZED.value, msg) class NotFoundError(ServiceError): """Not Found Error""" def __init__(self, msg: str = "Not found"): - super().__init__(404, msg) + super().__init__(HTTPStatus.NOT_FOUND.value, msg) class InternalServerError(ServiceError): """Internal Server Error""" def __init__(self, message: str): - super().__init__(500, message) + super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR.value, message) class ProxyEventType(Enum): @@ -511,10 +512,10 @@ def _not_found(self, method: str) -> ResponseBuilder: return ResponseBuilder( Response( - status_code=404, + status_code=HTTPStatus.NOT_FOUND.value, content_type=APPLICATION_JSON, headers=headers, - body=self._json_dump({"code": 404, "message": "Not found"}), + body=self._json_dump({"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}), ) ) @@ -527,7 +528,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: Response( status_code=e.status_code, content_type=APPLICATION_JSON, - body=self._json_dump({"code": e.status_code, "message": e.msg}), + body=self._json_dump({"statusCode": e.status_code, "message": e.msg}), ), route, ) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 7ece2579bb8..aa2a632c051 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -517,7 +517,7 @@ def bad_request_error(): # AND status code equals 400 assert result["statusCode"] == 400 assert result["headers"]["Content-Type"] == APPLICATION_JSON - expected = {"code": 400, "message": "Missing required parameter"} + expected = {"statusCode": 400, "message": "Missing required parameter"} assert result["body"] == json_dump(expected) # GIVEN an UnauthorizedError @@ -532,7 +532,7 @@ def unauthorized_error(): # AND status code equals 401 assert result["statusCode"] == 401 assert result["headers"]["Content-Type"] == APPLICATION_JSON - expected = {"code": 401, "message": "Unauthorized"} + expected = {"statusCode": 401, "message": "Unauthorized"} assert result["body"] == json_dump(expected) # GIVEN an NotFoundError @@ -547,7 +547,7 @@ def not_found_error(): # AND status code equals 404 assert result["statusCode"] == 404 assert result["headers"]["Content-Type"] == APPLICATION_JSON - expected = {"code": 404, "message": "Not found"} + expected = {"statusCode": 404, "message": "Not found"} assert result["body"] == json_dump(expected) # GIVEN an InternalServerError @@ -562,7 +562,7 @@ def internal_server_error(): # AND status code equals 500 assert result["statusCode"] == 500 assert result["headers"]["Content-Type"] == APPLICATION_JSON - expected = {"code": 500, "message": "Internal server error"} + expected = {"statusCode": 500, "message": "Internal server error"} assert result["body"] == json_dump(expected) # GIVEN an ServiceError with a custom status code @@ -578,5 +578,5 @@ def service_error(): assert result["statusCode"] == 502 assert result["headers"]["Content-Type"] == APPLICATION_JSON assert "Access-Control-Allow-Origin" in result["headers"] - expected = {"code": 502, "message": "Something went wrong!"} + expected = {"statusCode": 502, "message": "Something went wrong!"} assert result["body"] == json_dump(expected) From 9a0151465ab4c67cbe97c0d9cdf9986df12d87e9 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 6 Jul 2021 00:05:41 -0700 Subject: [PATCH 6/7] refactor: create exceptions and content_types --- .../event_handler/__init__.py | 3 +- .../event_handler/api_gateway.py | 53 ++----------------- .../event_handler/content_types.py | 2 + .../event_handler/exceptions.py | 45 ++++++++++++++++ .../event_handler/test_api_gateway.py | 36 +++++++------ 5 files changed, 73 insertions(+), 66 deletions(-) create mode 100644 aws_lambda_powertools/event_handler/content_types.py create mode 100644 aws_lambda_powertools/event_handler/exceptions.py diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 0475982e377..def92f706f9 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -2,6 +2,7 @@ Event handler decorators for common Lambda events """ +from .api_gateway import ApiGatewayResolver from .appsync import AppSyncResolver -__all__ = ["AppSyncResolver"] +__all__ = ["AppSyncResolver", "ApiGatewayResolver"] diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 464d80bc58a..391b1e4a2c2 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -7,57 +7,14 @@ from http import HTTPStatus from typing import Any, Callable, Dict, List, Optional, Set, Union +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.exceptions import ServiceError from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) -APPLICATION_JSON = "application/json" - - -class ServiceError(Exception): - """Service Error""" - - def __init__(self, status_code: int, msg: str): - """ - Parameters - ---------- - status_code: int - Http status code - msg: str - Error message - """ - self.status_code = status_code - self.msg = msg - - -class BadRequestError(ServiceError): - """Bad Request Error""" - - def __init__(self, msg: str): - super().__init__(HTTPStatus.BAD_REQUEST.value, msg) - - -class UnauthorizedError(ServiceError): - """Unauthorized Error""" - - def __init__(self, msg: str): - super().__init__(HTTPStatus.UNAUTHORIZED.value, msg) - - -class NotFoundError(ServiceError): - """Not Found Error""" - - def __init__(self, msg: str = "Not found"): - super().__init__(HTTPStatus.NOT_FOUND.value, msg) - - -class InternalServerError(ServiceError): - """Internal Server Error""" - - def __init__(self, message: str): - super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR.value, message) class ProxyEventType(Enum): @@ -513,7 +470,7 @@ def _not_found(self, method: str) -> ResponseBuilder: return ResponseBuilder( Response( status_code=HTTPStatus.NOT_FOUND.value, - content_type=APPLICATION_JSON, + content_type=content_types.APPLICATION_JSON, headers=headers, body=self._json_dump({"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}), ) @@ -527,7 +484,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder: return ResponseBuilder( Response( status_code=e.status_code, - content_type=APPLICATION_JSON, + content_type=content_types.APPLICATION_JSON, body=self._json_dump({"statusCode": e.status_code, "message": e.msg}), ), route, @@ -548,7 +505,7 @@ def _to_response(self, result: Union[Dict, Response]) -> Response: logger.debug("Simple response detected, serializing return before constructing final response") return Response( status_code=200, - content_type=APPLICATION_JSON, + content_type=content_types.APPLICATION_JSON, body=self._json_dump(result), ) diff --git a/aws_lambda_powertools/event_handler/content_types.py b/aws_lambda_powertools/event_handler/content_types.py new file mode 100644 index 00000000000..33b4acae7cb --- /dev/null +++ b/aws_lambda_powertools/event_handler/content_types.py @@ -0,0 +1,2 @@ +APPLICATION_JSON = "application/json" +PLAIN_TEXT = "plain/text" diff --git a/aws_lambda_powertools/event_handler/exceptions.py b/aws_lambda_powertools/event_handler/exceptions.py new file mode 100644 index 00000000000..1e285366a32 --- /dev/null +++ b/aws_lambda_powertools/event_handler/exceptions.py @@ -0,0 +1,45 @@ +from http import HTTPStatus + + +class ServiceError(Exception): + """Service Error""" + + def __init__(self, status_code: int, msg: str): + """ + Parameters + ---------- + status_code: int + Http status code + msg: str + Error message + """ + self.status_code = status_code + self.msg = msg + + +class BadRequestError(ServiceError): + """Bad Request Error""" + + def __init__(self, msg: str): + super().__init__(HTTPStatus.BAD_REQUEST.value, msg) + + +class UnauthorizedError(ServiceError): + """Unauthorized Error""" + + def __init__(self, msg: str): + super().__init__(HTTPStatus.UNAUTHORIZED.value, msg) + + +class NotFoundError(ServiceError): + """Not Found Error""" + + def __init__(self, msg: str = "Not found"): + super().__init__(HTTPStatus.NOT_FOUND.value, msg) + + +class InternalServerError(ServiceError): + """Internal Server Error""" + + def __init__(self, message: str): + super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR.value, message) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index aa2a632c051..e542483d73e 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -5,16 +5,18 @@ from pathlib import Path from typing import Dict +from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import ( - APPLICATION_JSON, ApiGatewayResolver, - BadRequestError, CORSConfig, - InternalServerError, - NotFoundError, ProxyEventType, Response, ResponseBuilder, +) +from aws_lambda_powertools.event_handler.exceptions import ( + BadRequestError, + InternalServerError, + NotFoundError, ServiceError, UnauthorizedError, ) @@ -60,7 +62,7 @@ def test_api_gateway_v1(): def get_lambda() -> Response: assert isinstance(app.current_event, APIGatewayProxyEvent) assert app.lambda_context == {} - return Response(200, APPLICATION_JSON, json.dumps({"foo": "value"})) + return Response(200, content_types.APPLICATION_JSON, json.dumps({"foo": "value"})) # WHEN calling the event handler result = app(LOAD_GW_EVENT, {}) @@ -68,7 +70,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON def test_api_gateway(): @@ -98,7 +100,7 @@ def test_api_gateway_v2(): def my_path() -> Response: assert isinstance(app.current_event, APIGatewayProxyEventV2) post_data = app.current_event.json_body - return Response(200, "plain/text", post_data["username"]) + return Response(200, content_types.PLAIN_TEXT, post_data["username"]) # WHEN calling the event handler result = app(load_event("apiGatewayProxyV2Event.json"), {}) @@ -106,7 +108,7 @@ def my_path() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEventV2 assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == "plain/text" + assert result["headers"]["Content-Type"] == content_types.PLAIN_TEXT assert result["body"] == "tom" @@ -220,7 +222,7 @@ def test_compress(): @app.get("/my/request", compress=True) def with_compression() -> Response: - return Response(200, APPLICATION_JSON, expected_value) + return Response(200, content_types.APPLICATION_JSON, expected_value) def handler(event, context): return app.resolve(event, context) @@ -266,7 +268,7 @@ def test_compress_no_accept_encoding(): @app.get("/my/path", compress=True) def return_text() -> Response: - return Response(200, "text/plain", expected_value) + return Response(200, content_types.PLAIN_TEXT, expected_value) # WHEN calling the event handler result = app({"path": "/my/path", "httpMethod": "GET", "headers": {}}, None) @@ -332,7 +334,7 @@ def rest_func() -> Dict: # THEN automatically process this as a json rest api response assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON expected_str = json.dumps(expected_dict, separators=(",", ":"), indent=None, cls=Encoder) assert result["body"] == expected_str @@ -387,7 +389,7 @@ def another_one(): # THEN routes by default return the custom cors headers assert "headers" in result headers = result["headers"] - assert headers["Content-Type"] == APPLICATION_JSON + assert headers["Content-Type"] == content_types.APPLICATION_JSON assert headers["Access-Control-Allow-Origin"] == cors_config.allow_origin expected_allows_headers = ",".join(sorted(set(allow_header + cors_config._REQUIRED_HEADERS))) assert headers["Access-Control-Allow-Headers"] == expected_allows_headers @@ -516,7 +518,7 @@ def bad_request_error(): # THEN return the bad request error response # AND status code equals 400 assert result["statusCode"] == 400 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON expected = {"statusCode": 400, "message": "Missing required parameter"} assert result["body"] == json_dump(expected) @@ -531,7 +533,7 @@ def unauthorized_error(): # THEN return the unauthorized error response # AND status code equals 401 assert result["statusCode"] == 401 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON expected = {"statusCode": 401, "message": "Unauthorized"} assert result["body"] == json_dump(expected) @@ -546,7 +548,7 @@ def not_found_error(): # THEN return the not found error response # AND status code equals 404 assert result["statusCode"] == 404 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON expected = {"statusCode": 404, "message": "Not found"} assert result["body"] == json_dump(expected) @@ -561,7 +563,7 @@ def internal_server_error(): # THEN return the internal server error response # AND status code equals 500 assert result["statusCode"] == 500 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON expected = {"statusCode": 500, "message": "Internal server error"} assert result["body"] == json_dump(expected) @@ -576,7 +578,7 @@ def service_error(): # THEN return the service error response # AND status code equals 502 assert result["statusCode"] == 502 - assert result["headers"]["Content-Type"] == APPLICATION_JSON + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON assert "Access-Control-Allow-Origin" in result["headers"] expected = {"statusCode": 502, "message": "Something went wrong!"} assert result["body"] == json_dump(expected) From f198aced4132499003885586405e55589fff25b6 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 6 Jul 2021 00:07:47 -0700 Subject: [PATCH 7/7] refactor: rather use shorthand --- aws_lambda_powertools/event_handler/exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/exceptions.py b/aws_lambda_powertools/event_handler/exceptions.py index 1e285366a32..56ea3e764d1 100644 --- a/aws_lambda_powertools/event_handler/exceptions.py +++ b/aws_lambda_powertools/event_handler/exceptions.py @@ -21,25 +21,25 @@ class BadRequestError(ServiceError): """Bad Request Error""" def __init__(self, msg: str): - super().__init__(HTTPStatus.BAD_REQUEST.value, msg) + super().__init__(HTTPStatus.BAD_REQUEST, msg) class UnauthorizedError(ServiceError): """Unauthorized Error""" def __init__(self, msg: str): - super().__init__(HTTPStatus.UNAUTHORIZED.value, msg) + super().__init__(HTTPStatus.UNAUTHORIZED, msg) class NotFoundError(ServiceError): """Not Found Error""" def __init__(self, msg: str = "Not found"): - super().__init__(HTTPStatus.NOT_FOUND.value, msg) + super().__init__(HTTPStatus.NOT_FOUND, msg) class InternalServerError(ServiceError): """Internal Server Error""" def __init__(self, message: str): - super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR.value, message) + super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, message)