Exceptions & Exception Handling¶
Ellar includes a built-in exceptions middleware, known as ExceptionMiddleware, responsible for processing all exceptions that occur within an application. When an exception goes unhandled by your application code, it is intercepted by this middleware, which then automatically sends an appropriate, user-friendly response.
{
  "status_code": 403,
  "detail": "Forbidden"
}
DEBUG, the exception handling behavior differs. When current_config.DEBUG is True, the exception that is raised is shown to the client for easy error debugging. However, when current_config.DEBUG is False, a 500 error is returned to the client, as illustrated below: {
  "statusCode": 500,
  "message": "Internal server error"
}
Ellar manages various types of exceptions by default:
- HTTPException: Provided by- Starletteto handle HTTP errors.eg.- HTTPException(status_code, detail=None, headers=None)
- WebSocketException: Provided by- Starletteto manage websocket errors. eg- WebSocketException(code=1008, reason=None)
- RequestValidationException: Provided by- Pydanticfor validation of request data
-  APIException: It is a type of exception designed for REST API-based applications. It offers a more conceptual approach to handling errors and provides a simple interface for creating other custom exceptions in your application without requiring an additional exception handler.For example, from ellar.common import APIException from starlette import status class ServiceUnavailableException(APIException): status_code = status.HTTP_503_SERVICE_UNAVAILABLE code = 'service_unavailable'
Built-in APIExceptions¶
Ellar offers a set of standard exceptions that inherit from the base APIException. These exceptions are available within the ellar.common package and represent many of the most common HTTP exceptions:
- AuthenticationFailed
- ImproperConfiguration
- MethodNotAllowed
- NotAcceptable
- NotAuthenticated
- NotFound
- PermissionDenied
- UnsupportedMediaType
Throwing standard exceptions¶
Let's use the ServiceUnavailableException in our previous project.
For example, in the CarController, we have a get_all() method (a GET route handler). Let's assume that this route handler throws an exception for some reason. To demonstrate this, we'll hard-code it as follows:
# project_name/apps/car/controllers.py
@get()
def get_all(self):
    raise ServiceUnavailableException()
{
  "detail": "Service Unavailable"
}
ServiceUnavailableException, it produces a JSON response because Ellar has implemented an exception handler for any APIException exception type. We will see how to change the default exception handler later. Another error presentation is available on the APIException instance:
- .detail: returns the textual description of the error.
- get_full_details(): returns both the textual description and other information about the error.
>>> print(exc.detail)
Service Unavailable
>>> print(exc.get_full_details())
{'detail':'Service Unavailable','code':'service_unavailable', 'description': 'The server cannot process the request due to a high load'}
Exception Handlers¶
Exception Handlers are classes or functions that handle what response that is returned to the client for specific exception types.
Here is an example of an ExceptionHandler that handles HTTPException in the application:
import typing as t
from ellar.common.interfaces import IExceptionHandler, IHostContext
from starlette.exceptions import (
    HTTPException as StarletteHTTPException,
)
from starlette.responses import Response
class HTTPExceptionHandler(IExceptionHandler):
    exception_type_or_code = StarletteHTTPException
    async def catch(
        self, ctx: IHostContext, exc: StarletteHTTPException
    ) -> t.Union[Response, t.Any]:
        assert isinstance(exc, StarletteHTTPException)
        config = ctx.get_app().config
        if exc.status_code in {204, 304}:
            return Response(status_code=exc.status_code, headers=exc.headers)
        if isinstance(exc.detail, (list, dict)):
            data = exc.detail
        else:
            data = {"detail": exc.detail, "status_code": exc.status_code}  # type: ignore[assignment]
        return config.DEFAULT_JSON_CLASS(
            data, status_code=exc.status_code, headers=exc.headers
        )
HTTPExceptionHandler will be registered in a key-value data structure of exception handlers. Where exception_type_or_code is the key and the HTTPExceptionHandler class is the value.  During exception handling, HTTPExceptionHandler.catch method will be called when ExceptionMiddleware detect an exception of type HTTPException. And then, a JSON response is created and returned to the client.
Creating Custom Exception Handler¶
To create an exception handler for your custom exception, you need to create a class that follows the IExceptionHandler contract.
At the root project folder, create a file named custom_exceptions.py:
# project_name/custom_exceptions.py
import typing as t
from ellar.common import IExceptionHandler, IHostContext
from starlette.responses import Response
class MyCustomException(Exception):
    pass
class MyCustomExceptionHandler(IExceptionHandler):
    exception_type_or_code = MyCustomException
    async def catch(
        self, ctx: IHostContext, exc: MyCustomException
    ) -> t.Union[Response, t.Any]:
        app_config = ctx.get_app().config
        return app_config.DEFAULT_JSON_CLASS(
            {'detail': str(exc)}, status_code=400,
        )
IExceptionHandler Overview:
- exception_type_or_code: defines the- exception classOR- status codeas a key to identify an exception handler.
- catch(): define the exception handling code and response a- Responseobject to be returned to the client.
Creating Exception Handler for status code¶
Let's create a handler for the MethodNotAllowedException, which corresponds to the HTTP status code 405.
# project_name/apps/custom_exceptions.py
import typing as t
from ellar.common import IExceptionHandler, IHostContext, render_template
from starlette.responses import Response
from starlette.exceptions import HTTPException
class MyCustomException(Exception):
    pass
class MyCustomExceptionHandler(IExceptionHandler):
    exception_type_or_code = MyCustomException
    async def catch(
        self, ctx: IHostContext, exc: MyCustomException
    ) -> t.Union[Response, t.Any]:
        app_config = ctx.get_app().config
        return app_config.DEFAULT_JSON_CLASS(
            {'detail': str(exc)}, status_code=400,
        )
class ExceptionHandlerAction405(IExceptionHandler):
    exception_type_or_code = 405
    async def catch(
        self, ctx: IHostContext, exc: HTTPException
    ) -> t.Union[Response, t.Any]:
        context_kwargs = {}
        return render_template('405.html', **context_kwargs)
HTTP exception with a 405 status code, and we return a template, 405.html, as a response. Similarly, you can create an exception handler for the 500 status code that returns an HTML template. Info
Ellar will search for 405.html in all registered modules. So, within the car folder, create a templates folder and add 405.html.
Registering Exception Handlers¶
We have successfully created two exception handlers ExceptionHandlerAction405 and MyCustomExceptionHandler but they are not yet visible to the application.
In the config.py file, which holds application settings, you can define custom exception handlers to be registered with the ExceptionMiddlewareService during the application's bootstrapping process.
# project_name/config.py
import typing as t
from ellar.core import ConfigDefaultTypesMixin
from ellar.common import IExceptionHandler
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405
class BaseConfig(ConfigDefaultTypesMixin):
    EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [
        MyCustomExceptionHandler(),
        ExceptionHandlerAction405()
    ]
Alternatively, you can add exception handlers directly through the app instance in your application:
# project_name/server.py
import os
from ellar.common.constants import ELLAR_CONFIG_MODULE
from ellar.core.factory import AppFactory
from .root_module import ApplicationModule
from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405
application = AppFactory.create_from_app_module(
    ApplicationModule,
    config_module=os.environ.get(
        ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig"
    ),
)
application.add_exception_handler(
    MyCustomExceptionHandler(), 
    ExceptionHandlerAction405()
)
Override Default Exception Handler¶
We have seen how to create an exception handler for status codes or for a specific exception type. The same applies to when we want to override an existing exception handler in Ellar project.
For example:
# project_name/apps/custom_exceptions.py
import typing as t
from ellar.common import IHostContext, IExceptionHandler, APIException
from starlette.responses import Response
class OverrideAPIExceptionHandler(IExceptionHandler):
    exception_type_or_code = APIException
    async def catch(
        self, ctx: IHostContext, exc: APIException
    ) -> t.Union[Response, t.Any]:
        app_config = ctx.get_app().config
        return app_config.DEFAULT_JSON_CLASS(
            {'message': exc.detail}, status_code=exc.status_code,
        )
OverrideAPIExceptionHandler exception handler, it will become the default handler for the APIException exception type. Declaring Exception Handler as a function¶
In the previous section, we saw how to create a custom ExceptionHandler from IExceptionHandler. In this section, we'll achieve the same result using a plain function.
For example, let's say we have a function exception_handler_fun as shown below:
from starlette.responses import PlainTextResponse
from ellar.common import IExecutionContext
def exception_handler_fun(ctx: IExecutionContext, exc: Exception):
    return PlainTextResponse('Bad Request', status_code=400)
exception_handler_fun work as an ExceptionHandler, you will need to use CallableExceptionHandler from the ellar.common.exceptions package: from starlette.responses import PlainTextResponse
from ellar.common import IExecutionContext
from ellar.core.exceptions import CallableExceptionHandler, as_exception_handler
@as_exception_handler
def exception_400_handler(ctx: IExecutionContext, exc: Exception):
    return PlainTextResponse('Bad Request', status_code=400)
exception_400_handler Exception Handler to handle HTTP exceptions with a status code of 400. You can then register it as an exception handler in your configuration, as we did in the previous section: Additionally, We can create a handler to handle custom exception types, as shown below:
from starlette.responses import PlainTextResponse
from ellar.common import IExecutionContext
from ellar.core.exceptions import as_exception_handler
class CustomException(Exception):
    pass
@as_exception_handler(CustomException)
def exception_custom_handler(ctx: IExecutionContext, exc: Exception):
    return PlainTextResponse('Bad Request', status_code=400)
exception_custom_handler is configured to handle a custom exception type, CustomException.