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

ImproperlyConfiguredException

500

Used internally for configuration errors

ValidationException

400

Raised when validation or parsing failed

NotFoundException

404

HTTP status code 404

NotAuthorizedException

401

HTTP status code 401

PermissionDeniedException

403

HTTP status code 403

InternalServerException

500

HTTP status code 500

ServiceUnavailableException

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:

../_images/exception-handlers.jpg

Exception Handlers#

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.