|
1 | 1 | # **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