Wanted to bounce this idea off the sanic community. In an internal code base, we heavily use sanic's request/response middlewares. While great for pre/post-processing responses and filtering requests, some limitations with the split middleware design that we experienced are:
We ended up writing an extended yield_middleware decorator that use async generators introduced in python-3.6 that accepts async generator functions which can contain both request and response middleware code in the same function.
If this is something others in community believe can be a useful pattern, happy to make a PR with implementation / tests.
Example:
from sanic.response import text
from ..utils.custom_classes import Blueprint
yield_middleware_test: Blueprint = Blueprint(
'yield_middleware_test', url_prefix="/yield_test")
@yield_middleware_test.yield_middleware
async def log_state(request):
print(f"enter middleware, request: {request}")
response = yield
print(f"enter middleware, response: {response}")
@yield_middleware_test.get("/")
async def yield_test(request):
print("route handler")
return text("hi")
Sample implementation code:
import inspect
from typing import AsyncGenerator, Awaitable, Callable, Dict, Union
from uuid import uuid4 as uuid
from sanic import Blueprint as SanicBlueprint
AsyncOrGen = Union[AsyncGenerator, Awaitable]
class Blueprint(SanicBlueprint):
_yield_middleware_generators: Dict[str, AsyncOrGen] = {}
def yield_middleware(self, fn: Callable) -> Callable:
is_asyncgen_fn: bool = inspect.isasyncgenfunction(fn)
assert is_asyncgen_fn or inspect.isawaitable(fn), \
f"Passed object neither async nor asyncgen function. Invalid {fn}"
fn_id: str = uuid()
register_req_middleware: Callable = self.middleware("request")
register_res_middleware: Callable = self.middleware("response")
if not is_asyncgen_fn:
return register_req_middleware(fn)
faux_req_middleware: Callable = self._make_req_middleware(fn, fn_id)
register_req_middleware(faux_req_middleware)
faux_res_middleware: Callable = self._make_res_middleware(fn_id)
register_res_middleware(faux_res_middleware)
def _make_req_middleware(self, fn: Callable, fn_id: str) -> Callable:
middleware_map: dict = self._yield_middleware_generators
assert not middleware_map.get(fn_id), \
f"UUID collision for {fn_id} : {fn}"
async def faux_req_middleware(request):
middleware_gen: Generator = fn(request)
middleware_map[fn_id] = middleware_gen
return await middleware_gen.asend(None)
return faux_req_middleware
def _make_res_middleware(self, fn_id: str) -> Callable:
middleware_map: dict = self._yield_middleware_generators
async def faux_res_middleware(request, response):
# Get and remove gen
middleware_gen: Generator = middleware_map.pop(fn_id, None)
if not middleware_gen:
return
# Exhaust generator
result = None
while True:
try:
result = await middleware_gen.asend(response)
except StopAsyncIteration:
break
return result
return faux_res_middleware
Looks cool! Can you show a slightly more practical use case, and how it would be performed now vs with your update? Or if what you're doing is simply not possible now, then skip that part... 馃槃
Of course @r0fls
Currently:
@bp.middleware("request")
async def auth(request):
user = await get_user_from_db(request.form.get("user_id"))
if not user:
return text("Unauthorized", 401)
logging.log("Auth succeded")
@bp.middleware("response")
async def wrap_token(request, response):
user = await get_user_from_db(request.form.get("user_id"))
return html(f"""
<input type="hidden" id="some-user-token" value="{user.token}">
{response.body}
""")
With this change:
@bp.yield_middleware
async def wrap_token(request):
user = await get_user_from_db(request.form.get("user_id"))
if not user:
return text("Unauthorized", 401)
logging.log("Auth succeded")
response = yield # Request handler fired here
return html(f"""
<input type="hidden" id="some-user-token" value="{user.token}">
{response.body}
""")
That saves the extra db lookup but more importantly it is immediately clear how the entire middleware logic manipulates the request and response in the same function. Happy to give an even more elaborate example but I felt that this captures the essence.
@r0fls any update?
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is incorrect, please respond with an update. Thank you for your contributions.
Most helpful comment
Of course @r0fls
Currently:
With this change:
That saves the extra db lookup but more importantly it is immediately clear how the entire middleware logic manipulates the request and response in the same function. Happy to give an even more elaborate example but I felt that this captures the essence.