Skip to content

Commit 98bf42e

Browse files
authored
Merge pull request #131 from python-ellar/exception_doc_update
Exception Docs Update
2 parents c1418dc + 9a9b20b commit 98bf42e

File tree

20 files changed

+1289
-47
lines changed

20 files changed

+1289
-47
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`

docs/security/csrf.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ class Development(BaseConfig):
2929
]
3030

3131
```
32+
33+
## **CORS**
34+
Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.
35+
Under the hood, Ellar registers CORS Middleware and provides CORS options in application for CORS customization.
36+
See how to configure **CORS** [here](../overview/middleware.md#corsmiddleware)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# **Encryption and Hashing**
2+
3+
**Encryption** is the method of transforming data. This process changes the original information, referred to as plaintext,
4+
into an alternative form called ciphertext.
5+
The goal is to ensure that only authorized parties possess the capability to decrypt ciphertext back into plaintext and access the original information.
6+
Encryption doesn't inherently prevent interference but rather restricts the intelligible content from potential interceptors.
7+
Encryption is a bidirectional operation; what's encrypted can be decrypted using the correct key.
8+
9+
**Hashing** is the process of converting a given key into another value.
10+
A hash function is employed to create this new value following a mathematical algorithm.
11+
After hashing is applied, it should be practically impossible to reverse the process and derive the original
12+
input from the output.
13+
14+
## **Encryption**
15+
In Python, the [`cryptography`](https://pypi.org/project/cryptography/) library provides a user-friendly way to implement encryption.
16+
One common encryption scheme is Fernet, which offers symmetric key encryption.
17+
18+
For example,
19+
```python
20+
from cryptography.fernet import Fernet
21+
22+
# Generate a random encryption key
23+
key = Fernet.generate_key()
24+
25+
# Create a Fernet cipher object with the key
26+
cipher_suite = Fernet(key)
27+
28+
# Text to be encrypted
29+
plaintext = b"Hello, this is a secret message!"
30+
31+
# Encrypt the plaintext
32+
cipher_text = cipher_suite.encrypt(plaintext)
33+
34+
# Decrypt the ciphertext
35+
decrypted_text = cipher_suite.decrypt(cipher_text)
36+
37+
# Convert bytes to string for printing
38+
original_message = decrypted_text.decode("utf-8")
39+
40+
print("Original Message: ", plaintext)
41+
print("Encrypted Message: ", cipher_text)
42+
print("Decrypted Message: ", original_message)
43+
44+
```
45+
The provided Python example demonstrates this process, securing a message with encryption and then decrypting it using the same key.
46+
It's crucial to manage encryption keys securely in real applications to maintain the confidentiality and integrity of your data.
47+
48+
## **Hashing**
49+
For hashing, Ellar works with [passlib](https://pypi.org/project/passlib/) and [hashlib](https://docs.python.org/3/library/hashlib.html)
50+
to create a wrapper around some hashing algorithms listed below,
51+
52+
- **PBKDF2Hasher**: `pbkdf2_sha256` hashing algorithm wrapper
53+
- **PBKDF2SHA1Hasher**: `pbkdf2_sha1` hashing algorithm wrapper
54+
- **Argon2Hasher**: `argon2` hashing algorithm wrapper
55+
- **BCryptSHA256Hasher**: `bcrypt_sha256` hashing algorithm wrapper
56+
- **BCryptHasher**: `bcrypt` hashing algorithm wrapper
57+
- **ScryptHasher**: `scrypt` hashing algorithm wrapper
58+
- **MD5Hasher**: `md5` hashing algorithm wrapper
59+
60+
## **Password Hashing**
61+
Ellar provides two important utility functions: `make_password` for password hashing
62+
and `check_password` for password validation. Both of these functions are available in the
63+
`ellar.core.security.hashers` package.
64+
65+
```python
66+
def make_password(
67+
password: str|bytes,
68+
algorithm: str = "pbkdf2_sha256",
69+
salt: str|None = None,
70+
) -> str:
71+
pass
72+
73+
74+
def check_password(
75+
password: str|bytes,
76+
encoded: str,
77+
setter: Callable[..., Any]|None = None,
78+
preferred_algorithm: str = "pbkdf2_sha256",
79+
) -> bool:
80+
pass
81+
```
82+
83+
The `make_password` function takes plain text and generates a hashed result based on the provided hash algorithm.
84+
In the code snippet above, the default algorithm for `make_password` is `pbkdf2_sha256`.
85+
86+
The [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) algorithm with a SHA256 hash is a password stretching mechanism recommended by [NIST](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf).
87+
This should be sufficient for most users as it is quite secure and requires massive amounts of computing time to break.
88+
89+
!!! note
90+
All hashing wrappers are registered as `key-value` pairs and can be accessed by the algorithm names
91+
using the get_hasher utility function in the `ellar.core.security.hashers` package.
92+
93+
For an example,
94+
```python
95+
from ellar.core.security.hashers import make_password
96+
97+
## Using pbkdf2_sha256 - PBKDF2Hasher
98+
password_hash = make_password('mypassword1234', algorithm="pbkdf2_sha256", salt='seasalt')
99+
print(password_hash)
100+
# pbkdf2_sha256$870000$seasalt$XE8bb8u57rxvyv2SThRFtMg9mzJLff2wjm3J8kGgFVI=
101+
102+
## Using bcrypt_sha256 - BCryptSHA256Hasher
103+
password_hash = make_password('mypassword1234', algorithm="bcrypt_sha256", salt='20AmWL1wKJZAHPiI1HEk4k')
104+
print(password_hash)
105+
# bcrypt_sha256$$2b$12$20AmWL1wKJZAHPiI1HEk4eZuAlMGHkK1rw4oou26bnwGmAE8F0JGK
106+
```
107+
108+
On the other hand, you can check or validate a password using the `check_password` function.
109+
110+
```python
111+
from ellar.core.security.hashers import check_password
112+
113+
hash_secret = "bcrypt_sha256$$2b$12$20AmWL1wKJZAHPiI1HEk4eZuAlMGHkK1rw4oou26bnwGmAE8F0JGK"
114+
assert check_password('mypassword1234', hash_secret) # True
115+
```

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
]

0 commit comments

Comments
 (0)