Skip to content

Commit aad6e9e

Browse files
author
¨eadwinCode¨
committed
Updated exception handler docs
1 parent 7bbc3c5 commit aad6e9e

File tree

12 files changed

+362
-46
lines changed

12 files changed

+362
-46
lines changed

docs/overview/exception_handling.md

Lines changed: 149 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,56 @@
11
# **Exceptions & Exception Handling**
2-
The `ExceptionMiddleware` and `ExceptionMiddlewareService` handle all unhandled exceptions throughout an application and provide user-friendly responses.
2+
Ellar comes with a built-in exceptions middleware, `ExceptionMiddleware`, which is responsible for processing all exceptions across
3+
an application. When an exception is not handled by your application code, it is caught by this middleware, which
4+
then automatically sends an appropriate user-friendly response .
35

46
```json
57
{
68
"status_code": 403,
79
"detail": "Forbidden"
810
}
911
```
12+
And based on application config `DEBUG` value, the exception is raised during is `config.DEBUG`
13+
is True but when `config.DEBUG` a 500 error is returned as shown below:
14+
```json
15+
{
16+
"statusCode": 500,
17+
"message": "Internal server error"
18+
}
19+
```
1020

11-
Types of exceptions managed by default:
21+
Types of exceptions types managed by default:
1222

13-
- **`HTTPException`**: Provided by `Starlette` to handle HTTP errors
14-
- **`WebSocketException`**: Provided by `Starlette` to manage websocket errors
23+
- **`HTTPException`**: Provided by `Starlette` to handle HTTP errors.eg. `HTTPException(status_code, detail=None, headers=None)`
24+
- **`WebSocketException`**: Provided by `Starlette` to manage websocket errors. eg `WebSocketException(code=1008, reason=None)`
1525
- **`RequestValidationException`**: Provided by `Pydantic` for validation of request data
16-
- **`APIException`**: Handles HTTP errors and provides more context about the error.
17-
18-
## **HTTPException**
19-
20-
The `HTTPException` class provides a base class that you can use for any
21-
handled exceptions.
22-
23-
* `HTTPException(status_code, detail=None, headers=None)`
24-
25-
## **WebSocketException**
26-
27-
You can use the `WebSocketException` class to raise errors inside WebSocket endpoints.
28-
29-
* `WebSocketException(code=1008, reason=None)`
26+
- **`APIException`**: It is a type of exception for REST API based applications. It gives more concept to error and provides a simple interface for creating other custom exception needs in your application without having to create an extra exception handler.
3027

31-
You can set any code valid as defined [in the specification](https://tools.ietf.org/html/rfc6455#section-7.4.1){target="_blank"}.
32-
33-
## **APIException**
34-
It is a type of exception for REST API based applications. It gives more concept to error and provides a simple interface for creating other custom exception needs in your application without having to create an extra exception handler.
35-
36-
For example,
37-
38-
```python
39-
from ellar.common.exceptions import APIException
40-
from starlette import status
41-
42-
class ServiceUnavailableException(APIException):
43-
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
44-
code = 'service_unavailable'
45-
46-
```
47-
!!!hint
48-
You should only raise `HTTPException` and `APIException` inside routing or endpoints. Middleware classes should instead just return appropriate responses directly.
28+
For example,
4929

30+
```python
31+
from ellar.common import APIException
32+
from starlette import status
33+
34+
class ServiceUnavailableException(APIException):
35+
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
36+
code = 'service_unavailable'
37+
38+
```
39+
40+
### **Built-in APIExceptions**
41+
Ellar provides a set of standard exceptions that inherit from the base `APIException`.
42+
These are exposed from the `ellar.common` package, and represent many of the most common HTTP exceptions:
43+
44+
- `AuthenticationFailed`
45+
- `ImproperConfiguration`
46+
- `MethodNotAllowed`
47+
- `NotAcceptable`
48+
- `NotAuthenticated`
49+
- `NotFound`
50+
- `PermissionDenied`
51+
- `UnsupportedMediaType`
52+
53+
## **Throwing standard exceptions**
5054
Let's use this `ServiceUnavailableException` in our previous project.
5155

5256
For example, in the `CarController`, we have a `get_all()` method (a `GET` route handler).
@@ -81,6 +85,51 @@ Service Unavailable
8185
{'detail':'Service Unavailable','code':'service_unavailable', 'description': 'The server cannot process the request due to a high load'}
8286
```
8387

88+
!!!hint
89+
You should only raise `HTTPException` and `APIException` during route function handling. Since exception manager is a
90+
middleware and `HTTPException` raised before the `ExceptionMiddleware` will not be managed. Its advice exceptions happening
91+
inside middleware classes should return appropriate responses directly.
92+
93+
94+
## **Exception Handlers**
95+
Exception Handlers are classes or functions that handles specific exception type response generation.
96+
97+
Below is an example of ExceptionHandler that handles `HTTPException` in the application:
98+
```python
99+
import typing as t
100+
101+
from ellar.common.interfaces import IExceptionHandler, IHostContext
102+
from starlette.exceptions import (
103+
HTTPException as StarletteHTTPException,
104+
)
105+
from starlette.responses import Response
106+
107+
108+
class HTTPExceptionHandler(IExceptionHandler):
109+
exception_type_or_code = StarletteHTTPException
110+
111+
async def catch(
112+
self, ctx: IHostContext, exc: StarletteHTTPException
113+
) -> t.Union[Response, t.Any]:
114+
assert isinstance(exc, StarletteHTTPException)
115+
config = ctx.get_app().config
116+
117+
if exc.status_code in {204, 304}:
118+
return Response(status_code=exc.status_code, headers=exc.headers)
119+
120+
if isinstance(exc.detail, (list, dict)):
121+
data = exc.detail
122+
else:
123+
data = {"detail": exc.detail, "status_code": exc.status_code} # type: ignore[assignment]
124+
125+
return config.DEFAULT_JSON_CLASS(
126+
data, status_code=exc.status_code, headers=exc.headers
127+
)
128+
```
129+
In the example above, `HTTPExceptionHandler.catch` method will be called when `ExeceptionMiddleware` detect exception of type `HTTPException`.
130+
And its return response to the client.
131+
132+
84133
## **Creating Custom Exception Handler**
85134

86135
To create an exception handler for your custom exception, you have to make a class that follows the `IExceptionHandler` contract.
@@ -227,3 +276,67 @@ class OverrideAPIExceptionHandler(IExceptionHandler):
227276
```
228277

229278
Once we register the `OverrideAPIExceptionHandler` exception handler, it will become the default handler for the `APIException` exception type.
279+
280+
## **Declaring Exception Handler as a function**
281+
In the previous section, we have seen how to create a custom ExceptionHandler from `IExceptionHandler`. In this section we will do the same using a plane function.
282+
283+
For example, lets say we have a function `exception_handler_fun` as shown below
284+
285+
```python
286+
from starlette.responses import PlainTextResponse
287+
from ellar.common import IExecutionContext
288+
289+
290+
def exception_handler_fun(ctx: IExecutionContext, exc: Exception):
291+
return PlainTextResponse('Bad Request', status_code=400)
292+
```
293+
294+
To get the `exception_handler_fun` to work as an ExceptionHandler, you will need `CallableExceptionHandler` from `ellar.common.exceptions` package.
295+
296+
```python
297+
from starlette.responses import PlainTextResponse
298+
from ellar.common import IExecutionContext
299+
from ellar.common.exceptions import CallableExceptionHandler
300+
301+
302+
def exception_handler_fun(ctx: IExecutionContext, exc: Exception):
303+
return PlainTextResponse('Bad Request', status_code=400)
304+
305+
306+
exception_400_handler = CallableExceptionHandler(
307+
exc_class_or_status_code=400, callable_exception_handler=exception_handler_fun
308+
)
309+
```
310+
In the above example, you have created `exception_400_handler` Exception Handler to handler http exceptions with status code 400.
311+
And then it can be registed as an exception handler as we did in previous section
312+
313+
```python
314+
from .custom_exception_handlers import exception_400_handler
315+
316+
317+
class BaseConfig(ConfigDefaultTypesMixin):
318+
EXCEPTION_HANDLERS: List[IExceptionHandler] = [
319+
exception_400_handler
320+
]
321+
```
322+
323+
Also, `exception_handler_fun` can be made to handle an custom exception type as shown below.
324+
```python
325+
from starlette.responses import PlainTextResponse
326+
from ellar.common import IExecutionContext
327+
from ellar.common.exceptions import CallableExceptionHandler
328+
329+
330+
class CustomException(Exception):
331+
pass
332+
333+
334+
def exception_handler_fun(ctx: IExecutionContext, exc: Exception):
335+
return PlainTextResponse('Bad Request', status_code=400)
336+
337+
338+
exception_custom_handler = CallableExceptionHandler(
339+
exc_class_or_status_code=CustomException, callable_exception_handler=exception_handler_fun
340+
)
341+
```
342+
In the above example, `exception_custom_handler`

ellar/cache/backends/serializer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def load(self, data: t.Any) -> t.Any:
2828
def dumps(self, data: t.Any) -> t.Any:
2929
# Only skip pickling for integers, an int subclasses as bool should be
3030
# pickled.
31-
if type(data) is int:
31+
if isinstance(data, int):
3232
return data
3333
return pickle.dumps(data, self._protocol)
3434

ellar/common/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import typing as t
22

3-
from starlette.exceptions import WebSocketException
4-
53
from .commands import EllarTyper, command
64
from .datastructures import UploadFile
75
from .decorators import (
@@ -30,6 +28,7 @@
3028
NotFound,
3129
PermissionDenied,
3230
UnsupportedMediaType,
31+
WebSocketException,
3332
)
3433
from .interfaces import (
3534
IApplicationShutdown,

ellar/common/exceptions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
PermissionDenied,
1212
UnsupportedMediaType,
1313
)
14+
from .callable_exceptions import CallableExceptionHandler
1415
from .context import ExecutionContextException, HostContextException
1516
from .validation import RequestValidationError, WebSocketRequestValidationError
1617

@@ -30,4 +31,5 @@
3031
"MethodNotAllowed",
3132
"NotAcceptable",
3233
"UnsupportedMediaType",
34+
"CallableExceptionHandler",
3335
]

ellar/common/exceptions/callable_exceptions.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class CustomException(Exception):
1919
2020
2121
def exception_handler_fun(ctx: IExecutionContext, exc: Exception):
22-
return PlainResponse('Bad Request', status_code=400)
22+
return PlainTextResponse('Bad Request', status_code=400)
2323
2424
exception_400_handler = CallableExceptionHandler(
2525
exc_class_or_status_code=400, callable_exception_handler= exception_handler_fun
@@ -41,9 +41,12 @@ def __init__(
4141
self,
4242
*func_args: t.Any,
4343
exc_class_or_status_code: t.Union[t.Type[Exception], int],
44-
callable_exception_handler: t.Callable[
45-
[IHostContext, Exception],
46-
t.Union[t.Awaitable[Response], Response, t.Any],
44+
callable_exception_handler: t.Union[
45+
t.Callable[
46+
[IHostContext, Exception],
47+
t.Union[t.Awaitable[Response], Response, t.Any],
48+
],
49+
t.Any,
4750
],
4851
) -> None:
4952
self.callable_exception_handler = callable_exception_handler

ellar/core/modules/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
TEMPLATE_FILTER_KEY,
99
TEMPLATE_GLOBAL_KEY,
1010
)
11-
from ellar.common.exceptions.callable_exceptions import CallableExceptionHandler
11+
from ellar.common.exceptions import CallableExceptionHandler
1212
from ellar.core.middleware import FunctionBasedMiddleware, Middleware
1313
from ellar.reflect import reflect
1414

ellar/core/security/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .hashers.base import (
2+
BasePasswordHasher,
3+
PBKDF2PasswordHasher,
4+
PBKDF2SHA1PasswordHasher,
5+
)
6+
7+
__all__ = [
8+
"BasePasswordHasher",
9+
"PBKDF2PasswordHasher",
10+
"PBKDF2SHA1PasswordHasher",
11+
]

0 commit comments

Comments
 (0)