From 9001b757698c25e320429acf2add0affdec49b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Rico?= Date: Fri, 4 Feb 2022 11:53:40 +0000 Subject: [PATCH 1/4] Convert any ChaliceViewError to a response --- chalice/app.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/chalice/app.py b/chalice/app.py index 0c3c7791e..f0c9a318f 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -106,6 +106,14 @@ def __init__(self, connection_id): class ChaliceViewError(ChaliceError): STATUS_CODE = 500 + def to_response(self): + return Response( + body={ + 'Code': self.__class__.__name__, + 'Message': str(self)}, + status_code=self.STATUS_CODE + ) + class ChaliceUnhandledError(ChaliceError): """This error is not caught from a Chalice view function. @@ -1760,9 +1768,7 @@ def _get_view_function_response(self, view_function, function_args): except ChaliceViewError as e: # Any chalice view error should propagate. These # get mapped to various HTTP status codes in API Gateway. - response = Response(body={'Code': e.__class__.__name__, - 'Message': str(e)}, - status_code=e.STATUS_CODE) + response = e.to_request() except Exception: response = self._unhandled_exception_to_response() return response From 409d8d4e177abdbdab5d28def2431f9b3ef79ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Rico?= Date: Fri, 4 Feb 2022 12:04:11 +0000 Subject: [PATCH 2/4] Fixed spelling of function --- chalice/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chalice/app.py b/chalice/app.py index f0c9a318f..99aec8f69 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -1768,7 +1768,7 @@ def _get_view_function_response(self, view_function, function_args): except ChaliceViewError as e: # Any chalice view error should propagate. These # get mapped to various HTTP status codes in API Gateway. - response = e.to_request() + response = e.to_response() except Exception: response = self._unhandled_exception_to_response() return response From 7ecbb36801e23984b3b38bb5caa94441e773fc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Rico?= Date: Thu, 17 Feb 2022 11:49:19 +0000 Subject: [PATCH 3/4] Add tests for custom error Exception --- tests/unit/test_app.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index ea33f3ce3..6c1c93a6d 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -19,6 +19,7 @@ from chalice.test import Client from chalice.app import ( APIGateway, + ChaliceViewError, Request, Response, handle_extra_types, @@ -1042,6 +1043,71 @@ def notfound(): assert json_response_body(raw_response)['Code'] == 'NotFoundError' +def test_chalice_view_custom_error_in_non_debug_mode(sample_app, create_event): + class CustomError(ChaliceViewError): + message = None + + def __init__(self, code, message): + super().__init__() + self.STATUS_CODE = code + self.message = message + + def to_response(self): + return Response( + body=dict( + error=self.__class__.__name__, + custom_msg=str(self.message) + ), + status_code=self.STATUS_CODE, + ) + + err_code = 503 + err_msg = "Method not available" + + @sample_app.route('/internalerr') + def internalerr(): + raise CustomError(err_code, err_msg) + + event = create_event('/internalerr', 'GET', {}) + raw_response = sample_app(event, context=None) + assert raw_response['statusCode'] == err_code + assert json_response_body(raw_response)['error'] == 'CustomError' + assert json_response_body(raw_response)['custom_msg'] == err_msg + + +def test_chalice_view_custom_error_in_debug_mode(sample_app, create_event): + class CustomError(ChaliceViewError): + message = None + + def __init__(self, code, message): + super().__init__() + self.STATUS_CODE = code + self.message = message + + def to_response(self): + return Response( + body=dict( + error=self.__class__.__name__, + custom_msg=str(self.message) + ), + status_code=self.STATUS_CODE, + ) + + err_code = 503 + err_msg = "Method not available" + + @sample_app.route('/internalerr') + def internalerr(): + raise CustomError(err_code, err_msg) + sample_app.debug = True + + event = create_event('/internalerr', 'GET', {}) + raw_response = sample_app(event, context=None) + assert raw_response['statusCode'] == err_code + assert json_response_body(raw_response)['error'] == 'CustomError' + assert json_response_body(raw_response)['custom_msg'] == err_msg + + def test_case_insensitive_mapping(): mapping = app.CaseInsensitiveMapping({'HEADER': 'Value'}) From d2ff3c97a980b9cd65ac218fe007aa9eed158bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Rico?= Date: Thu, 17 Feb 2022 12:25:56 +0000 Subject: [PATCH 4/4] Added docs for custom errors --- docs/source/tutorials/basicrestapi.rst | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/source/tutorials/basicrestapi.rst b/docs/source/tutorials/basicrestapi.rst index e6dfea396..2be68e37a 100644 --- a/docs/source/tutorials/basicrestapi.rst +++ b/docs/source/tutorials/basicrestapi.rst @@ -211,6 +211,60 @@ You can import these directly from the ``chalice`` package: from chalice import UnauthorizedError +Custom errors +^^^^^^^^^^^^^ + +You can create a custom response by implementing your own exception class. +The response must be generated by the method ``to_response`` and be +inherited from ``ChaliceViewError``. A sample custom error should look +like this: + +.. code-block:: python + + from chalice import ChaliceViewError, Response + + class GoneCity(ChaliceViewError): + city = None + STATUS_CODE = 410 + + def __init__(self, city): + super().__init__() + self.city = city + + def to_response(self): + return Response( + body=dict( + error=self.__class__.__name__, + message=f"The city of {self.city} has been abandoned", + city=self.city, + ), + status_code=self.STATUS_CODE, + ) + + @app.route('/cities/{city}') + def state_of_city(city): + try: + if city in ABANDONED_CITIES: + raise GoneCity(city) + return {'state': CITIES_TO_STATE[city]} + except KeyError: + raise BadRequestError("Unknown city '%s', valid choices are: %s" % ( + city, ', '.join(CITIES_TO_STATE.keys()))) + + +Save and deploy these changes:: + + $ chalice deploy + $ http https://endpoint/api/cities/petra + HTTP/1.1 410 Gone + + { + "error": "Gone", + "message": "The city of Petra has been abandoned", + "city": "petra" + } + + Additional Routing ------------------