Skip to content

[Django + ASGI] middleware breaks when getting user from request #355

@qbey

Description

@qbey

Issue

Since 6af129f (v 6.7.10) it seems the middleware does not manage asynchronous request properly using Django + Uvicorn

Context

Python 3.13

django==5.2.7
posthog==6.7.10
uvicorn==0.38.0

Workaround

While it does not work for me, I override the original Middleware to add this (note: the missing part of code is basically a copy-paste of the original methods):

class AsyncPosthogContextMiddleware(PosthogContextMiddleware):
    """
    Asynchronous Django middleware to extract PostHog context from HTTP requests.

    While the original PosthogContextMiddleware is supposed to manage both sync and async requests,
    the call to request.user fails in async contexts.
    """

    async def extract_tags_async(self, request):  # <== New method
        # type: (HttpRequest) -> Dict[str, Any]
        tags = {}

        (user_id, user_email) = await self.extract_request_user_async(request) # <== Changed line

        ...  # Original lines

    async def extract_request_user_async(self, request):  # <== New method
        user_id = None
        email = None

        user = await request.auser() # <== Changed line

        if user and getattr(user, "is_authenticated", False):
            try:
                user_id = str(user.pk)
            except Exception:  # noqa: BLE001, S110  # pylint: disable=broad-except
                pass

            try:
                email = str(user.email)
            except Exception:  # noqa: BLE001, S110  # pylint: disable=broad-except
                pass

        return user_id, email

    async def __acall__(self, request):
        # type: (HttpRequest) -> Awaitable[HttpResponse]
        """
        Asynchronous entry point for async request handling.

        This method is called when the middleware chain is async.
        """
        if self.request_filter and not self.request_filter(request):
            return await self.get_response(request) # <== Changed line

        ... # Original lines

I know this is bad and not future-proof, but I did not found a better way to switch to async nicely (ie, without wrapping everything inside sync_to_async) while preserving the original middleware.

Traceback

Traceback (most recent call last):
  File "/.venv/lib/python3.13/site-packages/posthog/contexts.py", line 126, in new_context
    yield
  File "/.venv/lib/python3.13/site-packages/posthog/integrations/django.py", line 219, in __acall__
    for k, v in self.extract_tags(request).items():
                ~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/posthog/integrations/django.py", line 117, in extract_tags
    (user_id, user_email) = self.extract_request_user(request)
                            ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/posthog/integrations/django.py", line 174, in extract_request_user
    if user and getattr(user, "is_authenticated", False):
       ^^^^
  File "/.venv/lib/python3.13/site-packages/django/utils/functional.py", line 251, in inner
    self._setup()
    ~~~~~~~~~~~^^
  File "/.venv/lib/python3.13/site-packages/django/utils/functional.py", line 404, in _setup
    self._wrapped = self._setupfunc()
                    ~~~~~~~~~~~~~~~^^
  File "/.venv/lib/python3.13/site-packages/django/contrib/auth/middleware.py", line 40, in <lambda>
    request.user = SimpleLazyObject(lambda: get_user(request))
                                            ~~~~~~~~^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/django/contrib/auth/middleware.py", line 20, in get_user
    request._cached_user = auth.get_user(request)
                           ~~~~~~~~~~~~~^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/django/contrib/auth/__init__.py", line 311, in get_user
    user = backend.get_user(user_id)
  File "/.venv/lib/python3.13/site-packages/django/contrib/auth/backends.py", line 232, in get_user
    user = UserModel._default_manager.get(pk=user_id)
  File "/.venv/lib/python3.13/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/django/db/models/query.py", line 629, in get
    num = len(clone)
  File "/.venv/lib/python3.13/site-packages/django/db/models/query.py", line 366, in __len__
    self._fetch_all()
    ~~~~~~~~~~~~~~~^^
  File "/.venv/lib/python3.13/site-packages/django/db/models/query.py", line 1949, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
                         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.venv/lib/python3.13/site-packages/django/db/models/query.py", line 91, in __iter__
    results = compiler.execute_sql(
        chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
    )
  File "/.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py", line 1621, in execute_sql
    cursor = self.connection.cursor()
  File "/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 24, in inner
    raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions