|
| 1 | + |
| 2 | +Ellar `ExceptionMiddleware` together with `ExceptionMiddlewareService` are responsible for processing all unhandled exception |
| 3 | +across the application and provides an appropriate user-friendly response. |
| 4 | + |
| 5 | +```json |
| 6 | +{ |
| 7 | + "status_code": 403, |
| 8 | + "detail": "Forbidden" |
| 9 | +} |
| 10 | +``` |
| 11 | + |
| 12 | +Default exceptions types Managed by default: |
| 13 | + |
| 14 | +- **`HTTPException`**: Default exception class provided by `Starlette` for HTTP client |
| 15 | +- **`WebSocketException`**: Default websocket exception class also provided by `Starlette` for websocket connection |
| 16 | +- **`RequestValidationException`**: Request data validation exception provided by `Pydantic` |
| 17 | +- **`APIException`**: Custom exception created for typical REST API based application to provides more concept about the exception raised. |
| 18 | + |
| 19 | +## **HTTPException** |
| 20 | + |
| 21 | +The `HTTPException` class provides a base class that you can use for any |
| 22 | +handled exceptions. |
| 23 | + |
| 24 | +* `HTTPException(status_code, detail=None, headers=None)` |
| 25 | + |
| 26 | +## **WebSocketException** |
| 27 | + |
| 28 | +You can use the `WebSocketException` class to raise errors inside WebSocket endpoints. |
| 29 | + |
| 30 | +* `WebSocketException(code=1008, reason=None)` |
| 31 | + |
| 32 | +You can set any code valid as defined [in the specification](https://tools.ietf.org/html/rfc6455#section-7.4.1). |
| 33 | + |
| 34 | +## **APIException** |
| 35 | +As stated earlier, its an exception type for typical REST API based application. Its gives more concept to error and provides a |
| 36 | +simple interface for creating other custom exception need in your application. |
| 37 | + |
| 38 | +For example, |
| 39 | + |
| 40 | +```python |
| 41 | +from ellar.core.exceptions import APIException |
| 42 | +from starlette import status |
| 43 | + |
| 44 | +class ServiceUnavailableException(APIException): |
| 45 | + status_code = status.HTTP_503_SERVICE_UNAVAILABLE |
| 46 | + code = 'service_unavailable' |
| 47 | + |
| 48 | +``` |
| 49 | +!!!hint |
| 50 | + You should only raise `HTTPException` and `APIException` inside routing or endpoints. Middleware classes should instead just return appropriate responses directly. |
| 51 | + |
| 52 | +Let's use this `ServiceUnavailableException` in our previous project. |
| 53 | + |
| 54 | +For example, in the `DogsController`, we have a `get_all()` method (a `GET` route handler). |
| 55 | +Let's assume that this route handler throws an exception for some reason. To demonstrate this, we'll hard-code it as follows: |
| 56 | + |
| 57 | +```python |
| 58 | +# project_name/apps/dogs/controllers.py |
| 59 | + |
| 60 | +@get() |
| 61 | +def get_all(self): |
| 62 | + raise ServiceUnavailableException() |
| 63 | + |
| 64 | +``` |
| 65 | +Now, when you visit [http://127.0.0.1/dogs/](http://127.0.0.1/dogs/), you will get a JSON response. |
| 66 | +```json |
| 67 | +{ |
| 68 | + "detail": "Service Unavailable" |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +We have a JSON response because Ellar has an exception handler for `APIException`. This handler can be change to return some different. |
| 73 | +We shall how to do that on Overriding Default Exception Handlers. |
| 74 | + |
| 75 | +There is other error presentation available on `APIException` instance: |
| 76 | +- `.detail`: returns textual description of the error. |
| 77 | +- `get_full_details()`: returns both textual description and other information about the error. |
| 78 | + |
| 79 | +```shell |
| 80 | +>>> print(exc.detail) |
| 81 | +Service Unavailable |
| 82 | +>>> print(exc.get_full_details()) |
| 83 | +{'detail':'Service Unavailable','code':'service_unavailable', 'description': 'The server cannot process the request due to a high load'} |
| 84 | +``` |
| 85 | + |
| 86 | +## **Creating Custom Exception Handler** |
| 87 | + |
| 88 | +To create an exception handler for your custom exception, you have to create a class that follow `IExceptionHandler` contract. |
| 89 | + |
| 90 | +At the root project folder, create a file `custom_exceptions.py`, |
| 91 | + |
| 92 | +```python |
| 93 | +# project_name/custom_exceptions.py |
| 94 | +import typing as t |
| 95 | +from ellar.core.exceptions import IExceptionHandler |
| 96 | +from ellar.core.context import IExecutionContext |
| 97 | +from starlette.responses import Response |
| 98 | + |
| 99 | + |
| 100 | +class MyCustomException(Exception): |
| 101 | + pass |
| 102 | + |
| 103 | + |
| 104 | +class MyCustomExceptionHandler(IExceptionHandler): |
| 105 | + exception_type_or_code = MyCustomException |
| 106 | + |
| 107 | + async def catch( |
| 108 | + self, ctx: IExecutionContext, exc: MyCustomException |
| 109 | + ) -> t.Union[Response, t.Any]: |
| 110 | + app_config = ctx.get_app().config |
| 111 | + return app_config.DEFAULT_JSON_CLASS( |
| 112 | + {'detail': str(exc)}, status_code=400, |
| 113 | + ) |
| 114 | + |
| 115 | +``` |
| 116 | +- `exception_type_or_code`: defines the `exception class` OR `status code` to target when resolving exception handlers. |
| 117 | +- `catch()`: defines the handling code and response to be returned to the client. |
| 118 | + |
| 119 | +### **Creating Exception Handler for status code** |
| 120 | +Let's create a handler for `MethodNotAllowedException` which, according to HTTP code is `405`. |
| 121 | + |
| 122 | +```python |
| 123 | +# project_name/apps/custom_exceptions.py |
| 124 | +import typing as t |
| 125 | +from ellar.core.exceptions import IExceptionHandler |
| 126 | +from ellar.core.context import IExecutionContext |
| 127 | +from ellar.core import render_template |
| 128 | +from starlette.responses import Response |
| 129 | +from starlette.exceptions import HTTPException |
| 130 | + |
| 131 | +class MyCustomException(Exception): |
| 132 | + pass |
| 133 | + |
| 134 | + |
| 135 | +class MyCustomExceptionHandler(IExceptionHandler): |
| 136 | + exception_type_or_code = MyCustomException |
| 137 | + |
| 138 | + async def catch( |
| 139 | + self, ctx: IExecutionContext, exc: MyCustomException |
| 140 | + ) -> t.Union[Response, t.Any]: |
| 141 | + app_config = ctx.get_app().config |
| 142 | + return app_config.DEFAULT_JSON_CLASS( |
| 143 | + {'detail': str(exc)}, status_code=400, |
| 144 | + ) |
| 145 | + |
| 146 | + |
| 147 | +class ExceptionHandlerAction405(IExceptionHandler): |
| 148 | + exception_type_or_code = 405 |
| 149 | + |
| 150 | + async def catch( |
| 151 | + self, ctx: IExecutionContext, exc: HTTPException |
| 152 | + ) -> t.Union[Response, t.Any]: |
| 153 | + context_kwargs = {} |
| 154 | + return render_template('405.html', request=ctx.switch_to_request(), **context_kwargs) |
| 155 | +``` |
| 156 | +We have registered a handler for any `HTTPException` with status code `405` and we have chosen to return a template `405.html` as response. |
| 157 | + |
| 158 | +!!!info |
| 159 | + Ellar will look for `405.html` in all registered module. So `dogs` folder, create a `templates` folder and add `405.html`. |
| 160 | + |
| 161 | +The same way can create Handler for `500` error code. |
| 162 | + |
| 163 | + |
| 164 | +## **Registering Exception Handlers** |
| 165 | +We have successfully created two exception handlers `ExceptionHandlerAction405` and `MyCustomExceptionHandler` but they are not yet visible to the application. |
| 166 | + |
| 167 | +- `config.py`: The config file holds manage application settings including `EXCEPTION_HANDLERS` fields which defines all custom exception handlers used in the application. |
| 168 | + |
| 169 | +```python |
| 170 | +# project_name/config.py |
| 171 | +import typing as t |
| 172 | +from ellar.core import ConfigDefaultTypesMixin |
| 173 | +from ellar.core.exceptions import IExceptionHandler |
| 174 | +from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405 |
| 175 | + |
| 176 | +class BaseConfig(ConfigDefaultTypesMixin): |
| 177 | + EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [ |
| 178 | + MyCustomExceptionHandler(), |
| 179 | + ExceptionHandlerAction405() |
| 180 | + ] |
| 181 | +``` |
| 182 | +- `application instance`: You can also add exception through `app` instance. |
| 183 | + |
| 184 | +```python |
| 185 | +# project_name/server.py |
| 186 | + |
| 187 | +import os |
| 188 | + |
| 189 | +from ellar.constants import ELLAR_CONFIG_MODULE |
| 190 | +from ellar.core.factory import AppFactory |
| 191 | +from .root_module import ApplicationModule |
| 192 | +from .apps.custom_exceptions import MyCustomExceptionHandler, ExceptionHandlerAction405 |
| 193 | + |
| 194 | +application = AppFactory.create_from_app_module( |
| 195 | + ApplicationModule, |
| 196 | + config_module=os.environ.get( |
| 197 | + ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig" |
| 198 | + ), |
| 199 | +) |
| 200 | + |
| 201 | +application.add_exception_handler( |
| 202 | + MyCustomExceptionHandler(), |
| 203 | + ExceptionHandlerAction405() |
| 204 | +) |
| 205 | +``` |
| 206 | + |
| 207 | +## **Override Default Exception Handler** |
| 208 | +We have gone through how to create an exception handler for status code and specific exception type. |
| 209 | +So, override any exception handler follows the same pattern with a target to exception class |
| 210 | + |
| 211 | +for example: |
| 212 | + |
| 213 | +```python |
| 214 | +# project_name/apps/custom_exceptions.py |
| 215 | +import typing as t |
| 216 | +from ellar.core.exceptions import IExceptionHandler, APIException |
| 217 | +from ellar.core.context import IExecutionContext |
| 218 | +from starlette.responses import Response |
| 219 | + |
| 220 | + |
| 221 | +class OverrideAPIExceptionHandler(IExceptionHandler): |
| 222 | + exception_type_or_code = APIException |
| 223 | + |
| 224 | + async def catch( |
| 225 | + self, ctx: IExecutionContext, exc: APIException |
| 226 | + ) -> t.Union[Response, t.Any]: |
| 227 | + app_config = ctx.get_app().config |
| 228 | + return app_config.DEFAULT_JSON_CLASS( |
| 229 | + {'message': exc.detail}, status_code=exc.status_code, |
| 230 | + ) |
| 231 | +``` |
| 232 | + |
| 233 | +Once we register `OverrideAPIExceptionHandler` exception handler, it will become the default handler for `APIException` exception type. |
0 commit comments