Search code examples
python-3.xdjangodjango-middleware

Django Middleware: RecursionError when accessing `self.request.user` in database query wrapper


I'm testing my database query middleware (Django docs here) on a sample django app with a Postgres db. The app is the cookiecutter boilerplate. My goal with the middleware is simply to log the user ID for all database queries. Using Python3.9 and Django3.2.13. My middleware code is below:

# Middleware code
import logging

import django
from django.db import connection

django_version = django.get_version()
logger = logging.getLogger(__name__)


class Logger:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        with connection.execute_wrapper(QueryWrapper(request)):
            return self.get_response(request)

class QueryWrapper:
    def __init__(self, request):
        self.request = request

    def __call__(self, execute, sql, params, many, context):
        # print(self.request.user)
        return execute(sql, params, many, context)

If print(self.request.user.id) is commented out, everything works fine. However, I've found that uncommenting it, or any type of interaction with the user field in the self.request object, causes a Recursion Error:

RecursionError at /about/
maximum recursion depth exceeded
Request Method: GET
Request URL:    http://127.0.0.1:8000/about/
Django Version: 3.2.13
Exception Type: RecursionError
Exception Value:    
maximum recursion depth exceeded
Exception Location: /opt/homebrew/lib/python3.9/site-packages/django/db/models/sql/query.py, line 192, in __init__
Python Executable:  /opt/homebrew/opt/[email protected]/bin/python3.9
Python Version: 3.9.13

In the error page, that is followed by many repetitions of the below error:

During handling of the above exception ('SessionStore' object has no attribute '_session_cache'), another exception occurred:
/opt/homebrew/lib/python3.9/site-packages/django/contrib/sessions/backends/base.py, line 233, in _get_session
            return self._session_cache …

During handling of the above exception ('SessionStore' object has no attribute '_session_cache'), another exception occurred:
/opt/homebrew/lib/python3.9/site-packages/django/contrib/sessions/backends/base.py, line 233, in _get_session
            return self._session_cache …

From consulting other SO posts, it seems accessing the user field should work fine. I've checked that the django_session table exists, and my middleware is also located at the very bottom of my middlewares (that include "django.contrib.sessions.middleware.SessionMiddleware" and "django.contrib.auth.middleware.AuthenticationMiddleware")

What's wrong here?


Solution

  • The following things happen when you write self.request.user:

    1. The request is checked for an attribute _cached_user, if present the cached user is returned if not auth.get_user is called with the request.
    2. Using the session key, Django checks the session to get the authentication backend used for the current user. Here if you are using database based sessions a database query is fired.
    3. Using the above authentication backend Django makes a database query to get the current user using their ID.

    As noted from the above points, unless there is a cache hit this process is going to cause a database query.

    Using database instrumentation you have installed a wrapper around database queries, the problem is that this wrapper itself is trying to make more queries (trying to get the current user), causing it to call itself. One solution would be to get the current user before installing your wrapper function:

    class Logger:
        def __init__(self, get_response):
            self.get_response = get_response
    
        def __call__(self, request):
            _user_authenticated = request.user.is_authenticated # Making sure user is fetched (Lazy object)
            with connection.execute_wrapper(QueryWrapper(request)):
                return self.get_response(request)
    
    
    class QueryWrapper:
        def __init__(self, request):
            self.request = request
    
        def __call__(self, execute, sql, params, many, context):
            print(self.request.user)
            return execute(sql, params, many, context)