Async await in jinja2 templates in Litestar
The problem
I was building this personal project where I needed a globally accessible function to check if a user is logged in:
{% if await is_logged_in() %}
<div>Show profile information</div>
{% endif %}
To build this I would need to find the user based on the token:
from jinja2 import pass_context async def get_user(token): ... @pass_context async def is_logged_in(context): token = context["request"].cookies.get("session_token") return await get_user(token) is not None
Now we would have to register this in jinja2’s global namespace:
template_config.engine_instance.engine.globals.update( { "is_logged_in": is_logged_in } )
This is all good and fine but this doesn’t work at all because the await
keyword isn’t supported in jinja2 templates.
Understanding jinja2’s async integration
Jinja2 allows async await using enable_async=True flag when initializing the
environment like the following:
from jinja2 import Environment environment = Environment(..., enable_async=True)
Then we can use render_async to automatically await coroutines or iterate over
async generators inside templates.
{{ some_async_function() }}
Here, some_async_function() would be automatically awaited by jinja2’s engine
if it returns a awaitable task or coroutine. But this feature doesn’t work
when using the render method which is why this wouldn’t work in litestar’s
default template configuration. [Ref: litestar-org/litestar issue #651 on
GitHub]
The fix
The fix is not a fix at all. We need to change our perspective on how to use jinja2 properly. We will essentialy be generating all the required data before hand in the route and just pass the final values to the template’s context.
But what about the problem that we had before. We needed the is_logged_in
function to be globally available for all templates to prevent repetitive code
inside route definitions. Fortunately litestar framework has support for
various types of middleware logic:
- Using middlewares - https://docs.litestar.dev/latest/usage/middleware/index.html
- Using
before_requesthandlers - https://docs.litestar.dev/latest/usage/lifecycle-hooks.html#before-request - Guards - https://docs.litestar.dev/2/usage/security/guards.html
The best place to populate request.state variables is in the before_request
handler function as middleware runs on static files and unmatched routes while
guards are made specifically for authentication and authorization purposes.
First set the user
Before request handlers in Litestar follow a very specific convention where they take in a request parameter and can return any value. For our purpose we will be setting the user object to be used inside templates.
from litestar import Request async def populate_request_state(request: Request): request.state.user = None if token := request.cookies.get("session_token"): request.state.user = await get_user(token)
Then set before_request handler
The definition of function isn’t enough. We need to map it to the
before_request parameter when initializing the Litestar app object like the
following:
from litestar import Litestar app = Litestar(..., before_request=populate_request_state)
Now use it in templates
The request variable is automatically injected by the litestar framework
inside jinja2 templates which can be used in any template with no additional
code:
{% if request.state.user is not None %}
<span>{request.state.user.name} is logged in</span>
{% else %}
<a href="/login">Login</a>
{% endif %}
Learnings
I don’t usually read everything before hand which is why I found this pattern
through hit and trial. Initially I tried really hard to make async-await work
inside jinja templates but found that litestar doesn’t support it natively.
Integrating multiple libraries is hard and litestar devs have made an enormous
effort to simplify the devex but there are still a few rough edges here and
there. Now that I have learnt the RIGHT way to use request.state inside the
before_request handler, I don’t think I would really need the capability to
enable async-await in jinja2 templates when using litestar.
Support
I didn’t really answer what I mentioned in the title of this post. So feel free to let me know if there is a litestar native way to enable async-await support inside jinja2 templates.