Async await in jinja2 templates in Litestar

Home

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:

  1. Using middlewares - https://docs.litestar.dev/latest/usage/middleware/index.html
  2. Using before_request handlers - https://docs.litestar.dev/latest/usage/lifecycle-hooks.html#before-request
  3. 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.