Exception Handling
pest ships with a built-in exception handling layer designed to gracefully handle exceptions in your application.
This layer is responsible for transforming any exception raised while handling requests into a
friendly JSON
API response.
It works by defining different exception handlers that can be triggered based on the type of the exception. When there's not a specific handler for the exception type, the generic handler will be used.
By default, if an exception is unrecognized by any of the handlers, a generic error message will be
returned with a 500
status code.
{
"statusCode": 500,
"error": "Internal server error"
}
HTTP Exceptions
HTTP protocol provides a wide range of status codes to describe the result of a request. In most cases, you'll want to return a specific status code when an exception is thrown.
You might want, for example:
- return a
404
HTTP status code when a requested model is not found in the database. - return a
403
HTTP status code when a user is not authorized to perform an action. - return a
400
HTTP status code when the request sent by the client is invalid or malformed.
pest
(actually, starlette (opens in a new tab)) provides a convenient way to
raise exceptions that will be handled and translated into the appropriate HTTP response with
the correct status code: HTTPException
.
from pest import HTTPException
@get("/{id}")
def get(self, id: int) -> HauntedHouse:
house = self.service.get_by_id(id)
if not house:
raise HTTPException(status_code=404, detail="House not found")
In the above example, if the requested house is not found, a 404
HTTP response will be returned
with the following payload:
{
"statusCode": 404,
"error": "Not Found",
"message": "House not found"
}
Subclassing HTTP Exceptions
The eception handler that catches HTTPException
will also catch any subclass of it (unless a more
specific handler is defined).
This allows you to create your own exceptions that will be handled by the generic handler.
from pest.exceptions import HTTPException
class HouseNotFoundException(HTTPException):
def __init__(self, id: int):
super().__init__(status_code=404, detail=f"House {id} not found")
But in most cases, you won't need to, as pest
already provides a set of exceptions that you can
use out of the box.
Built-in HTTP Exceptions
pest provides a set of HTTPException
exception subclasses that you can use out of the box:
BadRequestException
(HTTP 400)UnauthorizedException
(HTTP 401)ForbiddenException
(HTTP 403)NotFoundException
(HTTP 404)MethodNotAllowedException
(HTTP 405)NotAcceptableException
(HTTP 406)ProxyAuthenticationRequiredException
(HTTP 407)RequestTimeoutException
(HTTP 408)ConflictException
(HTTP 409)GoneException
(HTTP 410)PreconditionFailedException
(HTTP 412)PayloadTooLargeException
(HTTP 413)UnsupportedMediaTypeException
(HTTP 415)ImATeapotException
(HTTP 418)UnprocessableEntityException
(HTTP 422)InternalServerErrorException
(HTTP 500)NotImplementedException
(HTTP 501)BadGatewayException
(HTTP 502)ServiceUnavailableException
(HTTP 503)GatewayTimeoutException
(HTTP 504)HttpVersionNotSupportedException
(HTTP 505)
So, instead of raising a generic HTTPException
, you can raise a more specific exception, which
helps to make your code more expressive.
from pest.exceptions import NotFoundException
@get("/{id}")
def get(self, id: int) -> HauntedHouse:
house = self.service.get_by_id(id)
if not house:
raise NotFoundException(detail="House not found")
Custom Exception Handlers
The exception handling layer is extensible, which means you can define your own exception handlers to handle specific exceptions or even override the built-in ones.
While the generic exception handler is enough for most cases, you might want additional control over how exceptions are handled in your application.
Let's say you want to return a custom JSON response when a HauntedHouseNotFoundException
is
raised.
async def haunted_house_not_found(
request: Request, exc: HauntedHouseNotFoundException
) -> JSONResponse:
"""Handles HauntedHouseNotFoundException exceptions"""
return JSONResponse(
status_code=404,
content={"message": f"House {exc.id} not found"},
)
To register this handler, you need to add it to the exception_handlers
list during the
application initialization.
app = Pest.create(
exception_handlers={
HauntedHouseNotFoundException: haunted_house_not_found
}
)
exception_handlers
is a dictionary where the keys are the exception types and the values are
corresponding exception handlers.
Now, when a HauntedHouseNotFoundException
is raised, the haunted_house_not_found
handler will
be called and the response returned by it will be sent to the client. In this case, the response
will be:
{
"message": "House 1 not found"
}
Other ways to register exception handlers
add_exception_handler
method
That's not the only way to register exception handlers. You can also use the add_exception_handler
method of the pest application instance.
app = Pest.create()
app.add_exception_handler(
HauntedHouseNotFoundException, haunted_house_not_found
)
pest also provides a decorator that can be used to register multiple exception handlers at once.
app = Pest.create()
self.add_exception_handlers(
[
(HauntedHouseNotFoundException, haunted_house_not_found),
(HauntedHouseAlreadyExistsException, haunted_house_already_exists),
]
)
exception_handler
decorator
You can also use the exception_handler
decorator to register exception handlers.
app = Pest.create()
@app.exception_handler(HauntedHouseNotFoundException)
async def haunted_house_not_found(
request: Request, exc: HauntedHouseNotFoundException
) -> JSONResponse:
"""Handles HauntedHouseNotFoundException exceptions"""
return JSONResponse(
status_code=404,
content={"message": f"House {exc.id} not found"},
)
Per-Controller and Per-Route Exception Filters
Attaching exception handlers to specific controllers or routes is yet not supported by pest
, but
it's on the roadmap.