Skip to content

Commit c034e71

Browse files
committed
Added exception handling doc
1 parent 0a540ae commit c034e71

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

docs/overview/exception_filters.md

Whitespace-only changes.

docs/overview/exception_handling.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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.

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ nav:
4141
- Providers: overview/providers.md
4242
- Modules: overview/modules.md
4343
- Middlewares: overview/middleware.md
44-
- Exception Filters: overview/exception_filters.md
44+
- Exception Handling: overview/exception_handling.md
4545
- Guards: overview/guards.md
4646
- Custom Decorators: overview/custom_decorators.md
4747
- How-to Guides:

0 commit comments

Comments
 (0)