diff --git a/docs/exception-handlers.md b/docs/exception-handlers.md index 442ce567..f0886ce7 100644 --- a/docs/exception-handlers.md +++ b/docs/exception-handlers.md @@ -30,3 +30,48 @@ endpoint is called and the exception is raised. {! ../docs_src/_shared/exceptions.md !} The same is applied also to [dependencies](./dependencies.md). + + +### Custom exception handlers + +We all know that Esmerald handles really well the exceptions by design but sometimes we might also +want to throw an error while doing some code logic that is not directly related with a `data` of +an handler. + +For example. + +```python +{!> ../docs_src/exception_handlers/example.py !} +``` + +This example is a not usual at all but it serves to show where an exception is raised. + +Esmerald offers **one** out of the box **custom exception handlers**: + +* **value_error_handler** - When you want the `ValueError` exception to be automatically parsed +into a JSON. + +```python +from esmerald.exception_handlers import value_error_handler +``` + +How it would look like the previous example using this custom exception handler? + +```python hl_lines="21-23" +{!> ../docs_src/exception_handlers/example_use.py !} +``` + +Or if you prefer to place it on a Gateway level. + +```python hl_lines="22-25" +{!> ../docs_src/exception_handlers/example_use_gateway.py !} +``` + +Or even specific only to the handler itself. + +```python hl_lines="14-16" +{!> ../docs_src/exception_handlers/example_use_handler.py !} +``` + +As you can see, you can use this exception handler directly or as usual, you can create one of +your own and apply on every [application level](./application/levels.md). diff --git a/docs_src/exception_handlers/example.py b/docs_src/exception_handlers/example.py new file mode 100644 index 00000000..c5876ec5 --- /dev/null +++ b/docs_src/exception_handlers/example.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +from esmerald import Esmerald, Gateway, JSONResponse, post + + +class DataIn(BaseModel): + id: int + name: str + + +@post("/create") +async def create(data: DataIn) -> JSONResponse: + # Simple validation to raise ValueError + if data.id > 20: + raise ValueError("The ID must be less than 20.") + + +app = Esmerald(routes=[Gateway(handler=create)]) diff --git a/docs_src/exception_handlers/example_use.py b/docs_src/exception_handlers/example_use.py new file mode 100644 index 00000000..34b308aa --- /dev/null +++ b/docs_src/exception_handlers/example_use.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, ValidationError + +from esmerald import Esmerald, Gateway, JSONResponse, post +from esmerald.exception_handlers import pydantic_validation_error_handler, value_error_handler + + +class DataIn(BaseModel): + id: int + name: str + + +@post("/create") +async def create(data: DataIn) -> JSONResponse: + # Simple validation to raise ValueError + if data.id > 20: + raise ValueError("The ID must be less than 20.") + + +app = Esmerald( + routes=[Gateway(handler=create)], + exception_handlers={ + ValueError: value_error_handler, + }, +) diff --git a/docs_src/exception_handlers/example_use_gateway.py b/docs_src/exception_handlers/example_use_gateway.py new file mode 100644 index 00000000..8e718805 --- /dev/null +++ b/docs_src/exception_handlers/example_use_gateway.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, ValidationError + +from esmerald import Esmerald, Gateway, JSONResponse, post +from esmerald.exception_handlers import pydantic_validation_error_handler, value_error_handler + + +class DataIn(BaseModel): + id: int + name: str + + +@post("/create") +async def create(data: DataIn) -> JSONResponse: + # Simple validation to raise ValueError + if data.id > 20: + raise ValueError("The ID must be less than 20.") + + +app = Esmerald( + routes=[ + Gateway( + handler=create, + exception_handlers={ + ValueError: value_error_handler, + }, + ) + ], +) diff --git a/docs_src/exception_handlers/example_use_handler.py b/docs_src/exception_handlers/example_use_handler.py new file mode 100644 index 00000000..183a26af --- /dev/null +++ b/docs_src/exception_handlers/example_use_handler.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, ValidationError + +from esmerald import Esmerald, Gateway, JSONResponse, post +from esmerald.exception_handlers import pydantic_validation_error_handler, value_error_handler + + +class DataIn(BaseModel): + id: int + name: str + + +@post( + "/create", + exception_handlers={ + ValueError: value_error_handler, + }, +) +async def create(data: DataIn) -> JSONResponse: + # Simple validation to raise ValueError + if data.id > 20: + raise ValueError("The ID must be less than 20.") + + +app = Esmerald( + routes=[Gateway(handler=create)], +) diff --git a/esmerald/applications.py b/esmerald/applications.py index 4f7cedd6..90fe96bb 100644 --- a/esmerald/applications.py +++ b/esmerald/applications.py @@ -15,7 +15,7 @@ from openapi_schemas_pydantic.v3_1_0 import Contact, License, SecurityScheme, Tag from openapi_schemas_pydantic.v3_1_0.open_api import OpenAPI -from pydantic import AnyUrl +from pydantic import AnyUrl, ValidationError from starlette.applications import Starlette from starlette.middleware import Middleware as StarletteMiddleware # noqa from starlette.types import Lifespan, Receive, Scope, Send @@ -28,6 +28,7 @@ from esmerald.datastructures import State from esmerald.exception_handlers import ( improperly_configured_exception_handler, + pydantic_validation_error_handler, validation_error_exception_handler, ) from esmerald.exceptions import ImproperlyConfigured, ValidationErrorException @@ -775,6 +776,8 @@ def get_default_exception_handlers(self) -> None: ValidationErrorException, validation_error_exception_handler ) + self.exception_handlers.setdefault(ValidationError, pydantic_validation_error_handler) + def build_routes_middleware( self, route: "RouteParent", middlewares: Optional[List["Middleware"]] = None ) -> List["Middleware"]: diff --git a/tests/exception_handlers/test_custom_exception_handlers.py b/tests/exception_handlers/test_custom_exception_handlers.py index 3c52c73c..76a2b48b 100644 --- a/tests/exception_handlers/test_custom_exception_handlers.py +++ b/tests/exception_handlers/test_custom_exception_handlers.py @@ -34,12 +34,12 @@ async def update() -> JSON: DataIn(name="Esmerald", email="test@esmerald.dev") -def test_pydantic_validation_error_handler_return_500(test_client_factory): +def test_pydantic_validation_error_handler_return_422(test_client_factory): with create_client( routes=[Gateway(handler=create)], ) as client: response = client.post("/create") - assert response.status_code == 500 + assert response.status_code == 422 def test_pydantic_validation_error_handler(test_client_factory): @@ -64,33 +64,12 @@ def test_pydantic_validation_error_handler(test_client_factory): assert "email" in locs -def test_value_error_handler_return_500(test_client_factory): - with create_client( - routes=[Gateway(handler=update)], - ) as client: - response = client.put("/update") - assert response.status_code == 500 - - -def test_value_error_handler(test_client_factory): - with create_client( - routes=[Gateway(handler=update)], - exception_handlers={ValueError: value_error_handler}, - ) as client: - response = client.put("/update") - assert response.status_code == 400 - - assert ( - response.json()["detail"][0]["msg"] - == "Value error, The name: Esmerald was successfully passed" - ) - - def test_value_error_handler_return_500_on_function(test_client_factory): with create_client( routes=[Gateway(handler=raised)], ) as client: response = client.post("/raise-error") + assert response.status_code == 500