Exceptions and exception handling#
Starlite define a base exception called StarliteException
which serves
as a basis to all other exceptions.
In general, Starlite will raise two types of exceptions:
Exceptions that arise during application init, which fall
Exceptions that are raised as part of the normal application flow, i.e. exceptions in route handlers, dependencies and middleware, that should be serialized in some fashion.
Configuration Exceptions#
For missing extra dependencies, Starlite will raise either
MissingDependencyException
. For example, if you try to use the
SQLAlchemyPlugin without having SQLAlchemy installed, this will be raised when you
start the application.
For other configuration issues, Starlite will raise
ImproperlyConfiguredException
with a message explaining the
issue.
Application Exceptions#
For application exceptions, Starlite uses the class HTTPException
,
which inherits from StarliteException
. This exception will be serialized
into a JSON response of the following schema:
{
"status_code": 500,
"detail": "Internal Server Error",
"extra": {}
}
Starlite also offers several pre-configured exception subclasses with pre-set error codes that you can use, such as:
Exception |
Status code |
Description |
---|---|---|
500 |
Used internally for configuration errors |
|
400 |
Raised when validation or parsing failed |
|
404 |
HTTP status code 404 |
|
401 |
HTTP status code 401 |
|
403 |
HTTP status code 403 |
|
500 |
HTTP status code 500 |
|
503 |
HTTP status code 503 |
When a value fails pydantic
validation, the result will be a ValidationException
with the extra
key set to the
pydantic validation errors. Thus, this data will be made available for the API consumers by default.
Exception handling#
Starlite handles all errors by default by transforming them into JSON responses. If the errors are instances of
HTTPException
, the responses will include the appropriate status_code
.
Otherwise, the responses will default to 500 - "Internal Server Error"
.
You can customize exception handling by passing a dictionary, mapping either status codes or exception classes to callables. For example, if you would like to replace the default exception handler with a handler that returns plain-text responses you could do this:
from starlite import HTTPException, MediaType, Request, Response, Starlite, get
from starlite.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
def plain_text_exception_handler(_: Request, exc: Exception) -> Response:
"""Default handler for exceptions subclassed from HTTPException."""
status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
detail = getattr(exc, "detail", "")
return Response(
media_type=MediaType.TEXT,
content=detail,
status_code=status_code,
)
@get("/")
async def index() -> None:
raise HTTPException(detail="an error occurred", status_code=400)
app = Starlite(
route_handlers=[index],
exception_handlers={HTTPException: plain_text_exception_handler},
)
# run: /
The above will define a top level exception handler that will apply the plain_text_exception_handler
function to all
exceptions that inherit from HTTPException
. You could of course be more granular:
from starlite import (
HTTPException,
MediaType,
Request,
Response,
Starlite,
ValidationException,
get,
)
from starlite.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
def validation_exception_handler(request: Request, exc: ValidationException) -> Response:
return Response(
media_type=MediaType.TEXT,
content=f"validation error: {exc.detail}",
status_code=400,
)
def internal_server_error_handler(request: Request, exc: Exception) -> Response:
return Response(
media_type=MediaType.TEXT,
content=f"server error: {exc}",
status_code=500,
)
def value_error_handler(request: Request, exc: ValueError) -> Response:
return Response(
media_type=MediaType.TEXT,
content=f"value error: {exc}",
status_code=400,
)
@get("/validation-error")
async def validation_error(some_query_param: str) -> None:
pass
@get("/server-error")
async def server_error() -> None:
raise HTTPException()
@get("/value-error")
async def value_error() -> None:
raise ValueError("this is wrong")
app = Starlite(
route_handlers=[validation_error, server_error, value_error],
exception_handlers={
ValidationException: validation_exception_handler,
HTTP_500_INTERNAL_SERVER_ERROR: internal_server_error_handler,
ValueError: value_error_handler,
},
)
# run: /validation-error
# run: /server-error
# run: /value-error
The choice whether to use a single function that has switching logic inside it, or multiple functions depends on your specific needs.
While it does not make much sense to have different functions with a top-level exception handling,
Starlite supports defining exception handlers on all layers of the app, with the lower layers overriding layer above
them. In the following example, the exception handler for the route handler function will only handle
the ValidationException
occurring within that route handler:
from starlite import (
HTTPException,
Request,
Response,
Starlite,
ValidationException,
get,
)
def app_exception_handler(request: Request, exc: HTTPException) -> Response:
return Response(
content={
"error": "server error",
"path": request.url.path,
"detail": exc.detail,
"status_code": exc.status_code,
},
status_code=500,
)
def router_handler_exception_handler(request: Request, exc: ValidationException) -> Response:
return Response(
content={"error": "validation error", "path": request.url.path},
status_code=400,
)
@get("/")
async def index() -> None:
raise HTTPException("something's gone wrong")
@get(
"/greet",
exception_handlers={ValidationException: router_handler_exception_handler},
)
async def greet(name: str) -> str:
return f"hello {name}"
app = Starlite(
route_handlers=[index, greet],
exception_handlers={HTTPException: app_exception_handler},
)
# run: /
# run: /greet
Exception handling layers#
Since Starlite allows users to define both exception handlers and middlewares in a layered fashion, i.e. on individual route handlers, controllers, routers or the app layer, multiple layers of exception handlers are required to ensure that exceptions are handled correctly:
As a result of the above structure, the exceptions raised by the ASGI Router itself, namely 404 Not Found
and 405 Method Not Allowed
are handled only by exception handlers defined on the app layer. Thus, if you want to affect
these exceptions, you will need to pass the exception handlers for them to the Starlite constructor and cannot use other
layers for this purpose.