Skip to content

Commit 59086c5

Browse files
committed
Added interceptor docs
1 parent 5a44800 commit 59086c5

File tree

1 file changed

+150
-1
lines changed

1 file changed

+150
-1
lines changed

docs/overview/interceptors.md

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,151 @@
11
# **Interceptors - [(AOP) technique](https://en.wikipedia.org/wiki/Aspect-oriented_programming){target="_blank"}**
2-
Aspect Oriented Programming (AOP) technique
2+
3+
An interceptor is a class annotated with the `@injectable()` decorator and implements the EllarInterceptor interface.
4+
During request-response cycle, interceptors are called after middleware execution before route handler execution.
5+
6+
Interceptors have a set of useful capabilities which are inspired by the [Aspect Oriented Programming (AOP) technique](https://en.wikipedia.org/wiki/Aspect-oriented_programming){target="_blank"} technique.
7+
They make it possible to:
8+
9+
- bind extra logic before / after method execution
10+
- transform the result returned from a function
11+
- transform the exception thrown from a function
12+
- extend the basic function behavior
13+
- completely override a function depending on specific conditions (e.g., for caching purposes)
14+
15+
## **Basic**
16+
Each interceptor implements the `intercept()` method, which takes two arguments.
17+
The first one is the `ExecutionContext` instance (exactly the same object as for [guards](./guards)) and
18+
`next_interceptor` awaitable function that executes the next interceptor in the execution chain.
19+
20+
```python
21+
import typing as t
22+
from abc import ABC, abstractmethod
23+
from ellar.common import IExecutionContext
24+
25+
26+
class EllarInterceptor(ABC):
27+
@abstractmethod
28+
async def intercept(
29+
self, context: IExecutionContext, next_interceptor: t.Callable[..., t.Coroutine]
30+
) -> t.Any:
31+
"""implementation comes here"""
32+
```
33+
!!! note
34+
`intercept` function of interceptor class is an asynchronous function.
35+
36+
## **Execution context**
37+
The `ExecutionContext` adds several new helper methods that provide additional details about the current execution process.
38+
These details can be helpful in building more generic interceptors that can work across a broad set of controllers, methods, and execution contexts.
39+
Learn more about `ExecutionContext` [here](../basics/execution-context).
40+
41+
## **Next Interceptor Handler**
42+
The second argument, `next_interceptor`, in `intercept` of EllarInterceptor class is used to invoke the route handler method at some point in your interceptor.
43+
If you don't call the `next_interceptor` method in your implementation of the `intercept()` method, the route handler method won't be executed at all.
44+
45+
This approach means that the `intercept()` method effectively wraps the request/response cycle.
46+
As a result, you may implement custom logic **both before and after** the execution of the final route handler.
47+
It's clear that you can write code in your `intercept()` method that executes before calling `next_interceptor()`,
48+
but how do you affect what happens afterward? depending on the nature of the data returned by `next_interceptor()`,
49+
further manipulation can be done before final response to the client.
50+
51+
Using Aspect Oriented Programming terminology, the invocation of the route handler
52+
(i.e., calling `next_interceptor()`) is called a Pointcut, indicating that it's the point at which our
53+
additional logic is inserted.
54+
55+
Consider, for example, an incoming `POST /car` request. This request is destined for the `create()` handler
56+
defined inside the `CarController`. If an interceptor which does not call the `next_interceptor()`
57+
method is called anywhere along the way, the `create()` method won't be executed.
58+
Once `next_interceptor()` is called, the `create()` handler will be triggered. And once the response is returned,
59+
additional operations can be performed on the data returned, and a final result returned to the client.
60+
61+
62+
## **Aspect interception**
63+
The first use case we'll look at is to use an interceptor to log user interaction (e.g., storing user calls, asynchronously dispatching events or calculating a timestamp).
64+
We show a simple LoggingInterceptor below:
65+
66+
```python
67+
import typing as t
68+
import logging
69+
import time
70+
from ellar.common import EllarInterceptor, IExecutionContext
71+
from ellar.di import injectable
72+
73+
74+
logger = logging.getLogger('ellar')
75+
76+
77+
@injectable()
78+
class LoggingInterceptor(EllarInterceptor):
79+
async def intercept(
80+
self, context: IExecutionContext, next_interceptor: t.Callable[..., t.Coroutine]
81+
) -> t.Any:
82+
logger.info('Before Route Handler Execution...')
83+
start_time = time.time()
84+
85+
res = await next_interceptor()
86+
logger.info(f'After Route Handler Execution.... {time.time() - start_time}s')
87+
return res
88+
```
89+
90+
!!! hint
91+
Interceptors, like controllers, providers, guards, and so on, can inject dependencies through their `constructor`.
92+
93+
## **Binding interceptors**
94+
In order to set up the interceptor, we use the `@UseInterceptors()` decorator imported from the `ellar.common` package.
95+
Like **guards**, interceptors can be controller-scoped, method-scoped, or global-scoped.
96+
97+
```python
98+
from ellar.common import UseInterceptors, Controller
99+
100+
@UseInterceptors(LoggingInterceptor)
101+
@Controller()
102+
class CarController:
103+
...
104+
```
105+
106+
Note that we passed the LoggingInterceptor type (instead of an instance), leaving responsibility for instantiation to the framework and enabling dependency injection.
107+
As with guards, we can also pass an in-place instance:
108+
109+
```python
110+
from ellar.common import UseInterceptors, Controller
111+
112+
@UseInterceptors(LoggingInterceptor())
113+
@Controller()
114+
class CarController:
115+
...
116+
```
117+
118+
As mentioned, the construction above attaches the interceptor to every handler declared by this controller.
119+
If we want to restrict the interceptor's scope to a single method, we simply apply the decorator at the method level.
120+
121+
In order to set up a global interceptor, we use the use_global_interceptors() method of the Ellar application instance:
122+
123+
```python
124+
from ellar.core import AppFactory
125+
126+
app = AppFactory.create_from_app_module(ApplicationModule)
127+
app.use_global_interceptors(LoggingInterceptor())
128+
# OR
129+
# app.use_global_interceptors(LoggingInterceptor)
130+
```
131+
132+
## **Exception Handling**
133+
You can also handle exception through on the process of request/response cycle before it gets handled by system exception handlers.
134+
135+
```python
136+
class CustomException(Exception):
137+
pass
138+
139+
140+
@injectable
141+
class InterceptCustomException(EllarInterceptor):
142+
async def intercept(
143+
self, context: IExecutionContext, next_interceptor: t.Callable[..., t.Coroutine]
144+
) -> t.Any:
145+
try:
146+
return await next_interceptor()
147+
except CustomException as cex:
148+
res = context.switch_to_http_connection().get_response()
149+
res.status_code = 400
150+
return {"message": str(cex)}
151+
```

0 commit comments

Comments
 (0)