Middlewares¶
Overview¶
The Middleware feature allows you to inject custom logic into the request/response cycle of an HTTP client library. Middlewares can be both synchronous and asynchronous, providing a versatile way to modify outgoing requests or handle incoming responses.
Usage¶
To create your middleware, extend the Middleware class and implement the call method.
Synchronous Middleware¶
Here's an example of a synchronous middleware that logs information before and after the call_next invocation.
import logging
from declarativex import Middleware
class FooMiddleware(Middleware):
def __call__(self, *, request, call_next):
logging.info("pre FooMiddleware")
response = call_next(request)
logging.info("post FooMiddleware")
return response
Asynchronous Middleware¶
For async operations, you can define the call method as asynchronous.
import logging
from declarativex import Middleware
class FooMiddleware(Middleware):
async def __call__(self, *, request, call_next):
logging.info("pre FooMiddleware")
response = await call_next(request)
logging.info("post FooMiddleware")
return response
Signature checking¶
The Middleware class uses a metaclass to check the signature of the call method. This ensures that the middleware is implemented correctly and that the executor can invoke it properly.
Inheriting from Middleware
¶
To create your own middleware, you simply inherit from the Middleware class and implement the call method. The Signature metaclass will automatically check the call method's signature at the time of class definition.
Here's how you could inherit from the Middleware class:
class CustomMiddleware(Middleware):
def __call__(self, *, request: RawRequest, call_next: Callable[[RawRequest], ReturnType]) -> ReturnType:
# Your custom logic here
Synchronous and Asynchronous Middleware Interactions¶
Sync in Async and Async in Sync¶
While both synchronous and asynchronous middlewares can be created, it's important to note that they can't be mixed within the same HTTP function. Attempting to do so will result in a runtime exception. Here's how this validation works in practice:
Sync Middleware in Async Function¶
If you try to use a synchronous middleware within an asynchronous function, you'll encounter a MisconfiguredException
.
import pytest
from declarativex import http, MisconfiguredException, Middleware
class FooMiddleware(Middleware):
def __call__(self, *, request, call_next):
...
class AsyncBarMiddleware(Middleware):
async def __call__(self, *, request, call_next):
...
@pytest.mark.asyncio
async def test_sync_async_middleware():
@http(
"GET",
"api/users",
base_url="https://example.com",
middlewares=[FooMiddleware(), AsyncBarMiddleware()]
)
async def get_users():
pass
with pytest.raises(MisconfiguredException) as exc:
await get_users()
assert str(exc.value) == "Cannot use sync middleware with async function"
Async Middleware in Sync Function¶
Likewise, using an asynchronous middleware within a synchronous function will also trigger a MisconfiguredException
.
import pytest
from declarativex import http, MisconfiguredException, Middleware
class FooMiddleware(Middleware):
def __call__(self, *, request, call_next):
...
class AsyncBarMiddleware(Middleware):
async def __call__(self, *, request, call_next):
...
def test_sync_async_middleware():
@http(
"GET",
"api/users",
base_url="https://example.com",
middlewares=[FooMiddleware(), AsyncBarMiddleware()]
)
def get_users():
pass
with pytest.raises(MisconfiguredException) as exc:
get_users()
assert str(exc.value) == (
"Cannot use sync middleware(FooMiddleware) with async function"
)
How the Validation Works¶
The middleware validation occurs at the runtime. It checks the async attribute, which is set by the Signature meta-class. The _async attribute specifies whether the __call_ method in the middleware is asynchronous or not. The executor then uses this attribute to determine if a middleware is valid for a given function type.
This ensures that the middleware type matches the function type (async-to-async or sync-to-sync), maintaining the integrity and expected behavior of the HTTP client library.
By strictly prohibiting the mixing of sync and async, the library ensures that developers avoid unexpected behavior or tricky debugging scenarios. This makes for a more robust and predictable development experience.
Examples¶
Simple in-memory cache for GET requests¶
Here's an example of a simple in-memory cache for GET requests. It uses a dictionary to store the responses and returns the cached response if the request URL is already in the cache.
from declarativex import Middleware
class CacheMiddleware(Middleware):
cache = {}
def __call__(self, *, request, call_next):
if request.method != "GET":
return call_next(request)
url = request.url()
if url in self.cache:
return self.cache.get(url)
response = call_next(request)
self.cache[url] = response
return response