question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Can't get the correct contextvar set in the request handler in middleware's dispatch method

See original GitHub issue

Checklist

  • The bug is reproducible against the latest release and/or master.
  • There are no similar issues or pull requests to fix it yet.

Describe the bug

I try to manage database session with ContextVar and want to close it in the middleware, but I always get none unless the session was set in the middleware (This is valid, but not all handlers require sqlalchemy session).

To reproduce

from contextvars import ContextVar
from fastapi import Request
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from starlette.middleware.base import BaseHTTPMiddleware
from src.config import settings


engine = create_engine(
    settings.SQLALCHEMY_DATABASE_URI,
)

session_var = ContextVar("session", default=None)


def get_session() -> Session:
    if (rst := session_var.get()) is None:
        rst = Session(engine, future=True)
        print("create a session: ", rst)
        session_var.set(rst)
    print("return a session: ", rst)
    return rst


class DBSessionMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # with get_session().begin():
        #     pass
        res = await call_next(request)
        # FIXME Can't get the correct contextvar, unless session is set here.
        print("session in context: ", session_var.get())
        if (session := session_var.get()) is not None:
            session.close()
            print("close the session: ", session)
        return res

Expected behavior

Get the session if it is used in the handler, close it

Actual behavior

Always get none

Debugging material

Environment

  • OS: WSL 2/Ubuntu 20.04
  • Python version: 3.8.5
  • Starlette version: 0.13.6 (via FastAP 0.61.1I)

Additional context

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
uriyyocommented, Mar 15, 2021

Hi @cglacet,

The main advantage of using contextvars is that you don’t need to pass request instance everywhere in order to access some ctx data.

If take into account example above, you can access to session from any place and it will be context-bound:

async def some_route():
    session = get_session()
    # do some staff with session

For more information please follow PEP-567

1reaction
uriyyocommented, Mar 15, 2021

Hi @ShoorDay,

The problem not with starlette, there is a problem with how you use contextvars. Basically, when you set contextvar inside a child task it won’t be available at the parent.

An example regarding this problem:

from asyncio import run, create_task
from contextvars import ContextVar
from typing import Optional

VAR: ContextVar[Optional[str]] = ContextVar('VAR', default=None)


async def child():
    print(f'before setting VAR: {VAR.get()}')
    VAR.set('value')
    print(f'after setting VAR: {VAR.get()}')


async def main():
    print(f'before call child: {VAR.get()}')
    await create_task(child())
    print(f'after call child: {VAR.get()}')


if __name__ == '__main__':
    run(main())

The result will be:

before call child: None
before setting: VAR None
after setting: VAR value
after call child: value

In order to make your code working using contextvars you should update code to smth like this:

from contextvars import ContextVar
from dataclasses import dataclass
from typing import Optional

from fastapi import Request
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from starlette.middleware.base import BaseHTTPMiddleware

from src.config import settings

engine = create_engine(
    settings.SQLALCHEMY_DATABASE_URI,
)

session_var: ContextVar[Optional['SessionHolder']] = ContextVar("session", default=None)


@dataclass
class SessionHolder:
    session: Optional[Session] = None

    @property
    def is_session_inited(self) -> bool:
        return self.session is not None

    def get_session(self) -> Session:
        if self.session is None:
            # self.session = Session(engine, future=True)
            self.session = Session(engine)
            print("create a session: ", self.session)

        print("return a session: ", self.session)
        return self.session


def get_session() -> Session:
    holder = session_var.get()
    return holder.get_session()


class DBSessionMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        session_var.set(SessionHolder())

        res = await call_next(request)

        print("session in context: ", session_var.get())
        if (holder := session_var.get()).is_session_inited:
            holder.get_session().close()
            print("close the session: ", holder)

        return res

If you have any questions or maybe I missed smth, feel free to ask)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Middleware - Starlette
The Starlette application class allows you to include the ASGI middleware in a way that ensures that it remains wrapped by the exception...
Read more >
Release 0.3.5 Tomasz Wojcik - starlette-context
Here an example of a plugin that extracts a Session from the request cookies, expects it to be encoded in base64, attempts to...
Read more >
fastapi-events - PyPI
An event dispatching/handling library for FastAPI, and Starlette. Features: straightforward API to emit events anywhere in your code; events are handled ...
Read more >
why do you need to bind a function in a constructor
handleChange = , can't I just use static functions for handleChange and call it directly with in the class onClick={handleRefreshClick}> ? I have...
Read more >
httprouter - Go Packages
Package httprouter is a trie based high performance HTTP request ... and per handler support (using closure) e.g. GET(path,middleware.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found