Issues with Middleware in Django 3.1 using ASGI

We’re running a bunch of Django sites, but seeing some weird behaviour in Django 3.1 if we’re using ASGI to serve the application.

We have the need to be able to access the current user from anywhere in our code. For this we’ve always used a custom Middleware that saved the user from the request into thread locals, and we’d then be able to access the current user from anywhere in the request/response cycle (from inside a model, for example).
We’ve since changed this into using ContextVar instead of thread locals.

The middleware itself looks something like this:

from django.utils.deprecation import MiddlewareMixin
class CurrentUserMiddleware(MiddlewareMixin):
    def process_request(self, request):
        set_current_user(getattr(request, 'user', None))

This middleware is placed after django.contrib.sessions.middleware.SessionMiddleware and django.contrib.auth.middleware.AuthenticationMiddleware and everything’s been working great for years.

After upgrading to Django 3.1, things were initially fine as well. However, we’ve then been starting migrating sites to ASGI (Gunicorn + Uvicorn worker class) instead of WSGI, and upon doing this, everything breaks.

It seems that accessing request.user at this point in the middleware somehow triggers a SynchronousOnlyOperation exception inside the authentication middleware in Django.

I’m assuming it’s because we somehow need to change our custom middleware, but I can’t quite make out why and how - does anyone have any pointers?

The full stack trace is as follows:

AttributeError: 'SessionStore' object has no attribute '_session_cache'
  File "django/contrib/sessions/backends/base.py", line 215, in _get_session
    return self._session_cache
SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
  File "asgiref/sync.py", line 191, in main_wrap
    _restore_context(context[0])
  File "asgiref/sync.py", line 23, in _restore_context
    if cvar.get() != context.get(cvar):
  File "django/utils/functional.py", line 240, in inner
    self._setup()
  File "django/utils/functional.py", line 376, in _setup
    self._wrapped = self._setupfunc()
  File "django/contrib/auth/middleware.py", line 23, in <lambda>
    request.user = SimpleLazyObject(lambda: get_user(request))
  File "django/contrib/auth/middleware.py", line 11, in get_user
    request._cached_user = auth.get_user(request)
  File "django/contrib/auth/__init__.py", line 174, in get_user
    user_id = _get_user_session_key(request)
  File "django/contrib/auth/__init__.py", line 58, in _get_user_session_key
    return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
  File "django/contrib/sessions/backends/base.py", line 65, in __getitem__
    return self._session[key]
  File "django/contrib/sessions/backends/base.py", line 220, in _get_session
    self._session_cache = self.load()
  File "django/contrib/sessions/backends/db.py", line 43, in load
    s = self._get_session_from_db()
  File "django/contrib/sessions/backends/db.py", line 32, in _get_session_from_db
    return self.model.objects.get(
  File "django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "django/db/models/query.py", line 425, in get
    num = len(clone)
  File "django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "django/db/models/query.py", line 1303, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "django/db/models/query.py", line 53, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "django/db/models/sql/compiler.py", line 1152, in execute_sql
    cursor = self.connection.cursor()
  File "django/utils/asyncio.py", line 24, in inner
    raise SynchronousOnlyOperation(message)

Have you looked at the docs for asynchronous middleware? Perhaps the ‘sync_and_async_middleware’ decorator + example might help.

The error is telling you that that it can not perform the sync ORM operation to fetch the user from the db within an async context. Within async code, you can use the sync_to_async wrapper.

@massover I’d been looking at the middleware docs, but was a bit confused as to why exactly it would be needed in this case.

What confused me, was this part of the docs:

Middleware can support any combination of synchronous and asynchronous requests. Django will adapt requests to fit the middleware’s requirements if it cannot support both, but at a performance penalty. […] By default, Django assumes that your middleware is capable of handling only synchronous requests.

It sounded to me like it should be no problem, since our middleware didn’t explicitly state itself as being an asynchronous middleware.

What I didn’t realise was that the old MiddlewareMixin that was used to make old-style middleware compatible with new-style middleware, was updated in Django 3.1 to add the property async_capable = True which I guess is what’s causing the issue.

I guess the quick fix here is to add async_capable = False to our middleware class.

Thank you for your input, it wasn’t at all obvious to me, and the problem would’ve been even bigger once we got to the bigger projects we have.

Much appreciated :slight_smile: