Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 - New exception handler #124

Merged
merged 6 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/exception-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
18 changes: 18 additions & 0 deletions docs_src/exception_handlers/example.py
Original file line number Diff line number Diff line change
@@ -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)])
24 changes: 24 additions & 0 deletions docs_src/exception_handlers/example_use.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
28 changes: 28 additions & 0 deletions docs_src/exception_handlers/example_use_gateway.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
],
)
26 changes: 26 additions & 0 deletions docs_src/exception_handlers/example_use_handler.py
Original file line number Diff line number Diff line change
@@ -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)],
)
11 changes: 8 additions & 3 deletions esmerald/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -846,13 +849,15 @@ def build_user_middleware_stack(self) -> List["StarletteMiddleware"]:
StarletteMiddleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
)
if self.cors_config:
user_middleware.append(StarletteMiddleware(CORSMiddleware, **self.cors_config.dict()))
user_middleware.append(
StarletteMiddleware(CORSMiddleware, **self.cors_config.model_dump())
)
if self.csrf_config:
user_middleware.append(StarletteMiddleware(CSRFMiddleware, config=self.csrf_config))

if self.session_config:
user_middleware.append(
StarletteMiddleware(SessionMiddleware, **self.session_config.dict())
StarletteMiddleware(SessionMiddleware, **self.session_config.model_dump())
)

handlers_middleware += self.router.middleware
Expand Down
58 changes: 29 additions & 29 deletions esmerald/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, List, Union
from typing import Union

from orjson import JSONDecodeError, loads
from orjson import loads
from pydantic import ValidationError
from starlette import status
from starlette.exceptions import HTTPException as StarletteHTTPException
Expand Down Expand Up @@ -38,27 +38,6 @@ async def http_exception_handler(
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)


def parse_non_serializable_objects_from_validation_error(values: List[Any]) -> List[Any]:
"""
Parses non serializable objects from the validation error extras.
"""
details = []
for detail in values:
detail_inputs = detail.get("input", None)
if not isinstance(detail_inputs, list):
details.append(detail)
continue

inputs = []
for input in detail_inputs:
if isinstance(input, object):
inputs.append(str(input.__class__.__name__))
detail["input"] = inputs
details.append(detail)

return details


async def validation_error_exception_handler(
request: Request, exc: ValidationError
) -> JSONResponse:
Expand All @@ -67,13 +46,8 @@ async def validation_error_exception_handler(

if extra:
errors_extra = exc.extra.get("extra", {})
try:
details = loads(errors_extra)
except (TypeError, JSONDecodeError):
details = parse_non_serializable_objects_from_validation_error(errors_extra)

return JSONResponse(
{"detail": exc.detail, "errors": details},
{"detail": exc.detail, "errors": errors_extra},
status_code=status_code,
)
else:
Expand All @@ -90,6 +64,9 @@ async def http_error_handler(_: Request, exc: ExceptionErrorMap) -> JSONResponse
async def improperly_configured_exception_handler(
request: Request, exc: ImproperlyConfigured
) -> StarletteResponse:
"""
When an ImproperlyConfiguredException is raised.
"""
status_code = (
exc.status_code
if isinstance(exc, StarletteHTTPException)
Expand All @@ -109,3 +86,26 @@ async def improperly_configured_exception_handler(
status_code=status_code,
headers=headers,
)


async def pydantic_validation_error_handler(
request: Request, exc: ValidationError
) -> JSONResponse:
"""
This handler is to be used when a pydantic validation error is triggered during the logic
of a code block and not the definition of a handler.

This is different from validation_error_exception_handler
"""
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
return JSONResponse({"detail": loads(exc.json())}, status_code=status_code)


async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse:
"""
Simple handler that manages all the ValueError exceptions thrown to the user properly
formatted.
"""
status_code = status.HTTP_400_BAD_REQUEST
details = loads(exc.json()) if hasattr(exc, "json") else exc.args[0]
return JSONResponse({"detail": details}, status_code=status_code)
2 changes: 1 addition & 1 deletion esmerald/middleware/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def create_exception_response(self, exc: Exception) -> Response:
content = ResponseContent(detail=repr(exc))
return Response(
media_type=MediaType.JSON,
content=content.dict(exclude_none=True),
content=content.model_dump(exclude_none=True),
status_code=content.status_code,
headers=exc.headers
if isinstance(exc, (HTTPException, StarletteHTTPException))
Expand Down
4 changes: 3 additions & 1 deletion esmerald/routing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,9 @@ def get_cookies(
filtered_cookies.append(cookie)
normalized_cookies: List[Dict[str, Any]] = []
for cookie in filtered_cookies:
normalized_cookies.append(cookie.dict(exclude_none=True, exclude={"description"}))
normalized_cookies.append(
cookie.model_dump(exclude_none=True, exclude={"description"})
)
return normalized_cookies

def get_headers(self, headers: "ResponseHeaders") -> Dict[str, Any]:
Expand Down
2 changes: 1 addition & 1 deletion esmerald/security/jwt/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def encode(self, key: str, algorithm: str) -> Union[str, Any]:
"""
try:
return jwt.encode(
claims=self.dict(exclude_none=True),
claims=self.model_dump(exclude_none=True),
key=key,
algorithm=algorithm,
)
Expand Down
3 changes: 2 additions & 1 deletion esmerald/transformers/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from inspect import Signature
from typing import Any, ClassVar, Optional, Set, Union

from orjson import loads
from pydantic import ValidationError

from esmerald.exceptions import ImproperlyConfigured, InternalServerError, ValidationErrorException
Expand Down Expand Up @@ -37,7 +38,7 @@ def build_exception(
server_errors = []
client_errors = []

for err in exception.errors():
for err in loads(exception.json()):
if not cls.is_server_error(err):
client_errors.append(err)
else:
Expand Down
31 changes: 0 additions & 31 deletions esmerald/transformers/helpers.py

This file was deleted.

4 changes: 0 additions & 4 deletions esmerald/transformers/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from esmerald.parsers import ArbitraryExtraBaseModel
from esmerald.transformers.constants import CLASS_SPECIAL_WORDS, VALIDATION_NAMES
from esmerald.transformers.datastructures import EsmeraldSignature, Parameter
from esmerald.transformers.helpers import is_pydantic_constrained_field
from esmerald.transformers.utils import get_field_definition_from_param
from esmerald.typing import Undefined
from esmerald.utils.dependency import is_dependency_field, should_skip_dependency_validation
Expand Down Expand Up @@ -75,9 +74,6 @@ def create_signature(self) -> Type[EsmeraldSignature]:
if self.skip_parameter_validation(param):
self.field_definitions[param.name] = (Any, ...)
continue
if is_pydantic_constrained_field(param.default):
self.field_definitions[param.name] = (param.default, ...)
continue
self.field_definitions[param.name] = get_field_definition_from_param(param)

model: Type["EsmeraldSignature"] = create_model(
Expand Down
Loading