Exception handling¶
Exception handlers are classes with methods mapped to exception types:
from patera import HttpStatus, Request, Response
from patera.exceptions import (
BaseHttpException,
ExceptionHandler,
RequestValidationError,
exception_handler,
handles,
)
class EntityNotFound(BaseHttpException):
def __init__(self, message: str):
super().__init__(message, HttpStatus.NOT_FOUND)
@exception_handler
class ApiExceptionHandler(ExceptionHandler):
@handles(RequestValidationError)
async def validation_error(self, req: Request, exc: RequestValidationError) -> Response:
return req.res.json({"message": "Invalid data", "details": exc.errors}).status(422)
@handles(BaseHttpException) # also handles subclasses, e.g. EntityNotFound
async def http_error(self, req: Request, exc: BaseHttpException) -> Response:
return req.res.json({"message": exc.message, "data": exc.data}).status(exc.status_code)
Put the class in an exceptions folder (file name containing handler) or register it with
app.register_exception_handler(...).
@handlesaccepts several exception classes and can be stacked.- A handler registered for a base class also handles its subclasses; lookup follows the exception's MRO and the most specific handler wins.
- Exceptions raised in middleware are handled by the exception handlers too.
A handler can return any response, for example RFC 9457 problem details:
@handles(MissingNote)
async def missing(self, req: Request, exc: MissingNote) -> Response:
return req.res.problem("Note not found", str(exc), 404)
Default responses¶
Without a matching handler:
| Exception | Response |
|---|---|
BaseHttpException and subclasses (abort(), authentication errors, ...) |
JSON {"status", "message", "data"} with the exception's status code |
RequestValidationError (from @consumes or req.validate(), a BaseHttpException) |
422 with {"status": "error", "message": "Validation failed", "data": [...]} |
anything else, including a pydantic.ValidationError from your own code or data |
DEBUG=True: the exception propagates (traceback in the server log). DEBUG=False: 500 {"status": "error", "message": "Internal server error"} and the error is logged |
Other built-in statuses: a malformed body is 400, a media type mismatch 415, an oversized
body 413.
Override default_exception_response(req, exc) on the application class to change this fallback.
Quick aborts¶
from patera import HttpStatus, abort, html_abort
abort("User not found", HttpStatus.NOT_FOUND, data={"id": user_id}) # JSON error response
html_abort("errors/404.html", HttpStatus.NOT_FOUND, {"id": user_id}) # renders the template
Special cases¶
HtmlAborterExceptionandStaticPageNotFoundhave special dispatch: register their exact classes to replace their handling. A genericBaseHttpExceptionhandler does not capture them.- Routing failures (
404,405) occur outside ordinary controller and middleware handling. - The framework resets unfinished responses before replacing them with an error. Once sending has started (for example a stream), failures propagate without sending a second response; disconnects cannot be answered either.
Response[Model]conversion does not apply to 4xx/5xx bodies. Build and validate an error schema explicitly if you need one.