#33533: Should it be impossible for a session dict to return more than one value
for a key?
----------------------------------+--------------------------------------
     Reporter:  Michael           |                    Owner:  nobody
         Type:  Uncategorized     |                   Status:  closed
    Component:  contrib.sessions  |                  Version:  4.0
     Severity:  Normal            |               Resolution:  invalid
     Keywords:                    |             Triage Stage:  Unreviewed
    Has patch:  0                 |      Needs documentation:  0
  Needs tests:  0                 |  Patch needs improvement:  0
Easy pickings:  0                 |                    UI/UX:  0
----------------------------------+--------------------------------------

Old description:

> In my project, I store the serial number of the device that logs in it's
> session. However in the code that gets the value from the session:
>
> {{{
> SESSION_SERIAL_NUMBER = 'terminal_serial_number'
>
> user._serial_number = request.session.get(SESSION_SERIAL_NUMBER, '')
> }}}
>
> Very occasionally throws an error. The part that baffles me is:
>
> > get() returned more than one Session -- it returned more than 20!
>
> How can a dict return more than one result, surely that is a bug for a
> dict to return more than one value?
>
> Note: When I query the database `django_sessions` table with the given
> `sessionid`, it only returns one row.
>
> Heres the error (from the email):
>
> {{{
> Internal Server Error: /accounts/login/
>
> ProgrammingError at /accounts/login/
> no results to fetch
> }}}
>
> And the traceback:
>
> {{{
> Traceback (most recent call last):
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 180, in
> _get_session
>     return self._session_cache
>
> During handling of the above exception ('SessionStore' object has no
> attribute '_session_cache'), another exception occurred:
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/core/handlers/exception.py", line 47, in inner
>     response = get_response(request)
>   File "./dist/terminals/middleware.py", line 11, in __call__
>     user._serial_number = request.session.get(SESSION_SERIAL_NUMBER, '')
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 65, in get
>     return self._session.get(key, default)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 185, in
> _get_session
>     self._session_cache = self.load()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/db.py", line 43, in load
>     s = self._get_session_from_db()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/db.py", line 32, in
> _get_session_from_db
>     return self.model.objects.get(
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/manager.py", line 85, in manager_method
>     return getattr(self.get_queryset(), name)(*args, **kwargs)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/query.py", line 443, in get
>     raise self.model.MultipleObjectsReturned(
>
> During handling of the above exception (get() returned more than one
> Session -- it returned more than 20!), another exception occurred:
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 180, in
> _get_session
>     return self._session_cache
>
> During handling of the above exception ('SessionStore' object has no
> attribute '_session_cache'), another exception occurred:
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/utils.py", line 97, in inner
>     return func(*args, **kwargs)
>
> The above exception (no results to fetch) was the direct cause of the
> following exception:
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/core/handlers/exception.py", line 47, in inner
>     response = get_response(request)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/utils/deprecation.py", line 126, in __call__
>     response = response or self.get_response(request)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/core/handlers/exception.py", line 49, in inner
>     response = response_for_exception(request, exc)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/core/handlers/exception.py", line 110, in
> response_for_exception
>     response = handle_uncaught_exception(request,
> get_resolver(get_urlconf()), sys.exc_info())
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/core/handlers/exception.py", line 149, in
> handle_uncaught_exception
>     return callback(request)
>   File "./core/base/views.py", line 152, in error_500_view
>     response = render(request, "core.base/500.html", context=context)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/shortcuts.py", line 19, in render
>     content = loader.render_to_string(template_name, context, request,
> using=using)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/template/loader.py", line 62, in render_to_string
>     return template.render(context, request)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/template/backends/django.py", line 61, in render
>     return self.template.render(context)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/template/base.py", line 174, in render
>     with context.bind_template(self):
>   File "/usr/lib/python3.8/contextlib.py", line 113, in __enter__
>     return next(self.gen)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/debug_toolbar/panels/templates/panel.py", line 46, in
> _request_context_bind_template
>     context = processor(self.request)
>   File "./core/base/context_processors.py", line 13, in project
>     menu = get_plug_menu(request.user)
>   File "./dist/plug/menu.py", line 58, in get_plug_menu
>     if user.is_anonymous or user.is_terminal:
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/utils/functional.py", line 248, in inner
>     self._setup()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/utils/functional.py", line 384, in _setup
>     self._wrapped = self._setupfunc()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/auth/middleware.py", line 25, in <lambda>
>     request.user = SimpleLazyObject(lambda: get_user(request))
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/auth/middleware.py", line 11, in get_user
>     request._cached_user = auth.get_user(request)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/auth/__init__.py", line 177, in get_user
>     user_id = _get_user_session_key(request)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/auth/__init__.py", line 60, in
> _get_user_session_key
>     return
> get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 50, in
> __getitem__
>     return self._session[key]
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/base.py", line 185, in
> _get_session
>     self._session_cache = self.load()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/db.py", line 43, in load
>     s = self._get_session_from_db()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/contrib/sessions/backends/db.py", line 32, in
> _get_session_from_db
>     return self.model.objects.get(
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/manager.py", line 85, in manager_method
>     return getattr(self.get_queryset(), name)(*args, **kwargs)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/query.py", line 435, in get
>     num = len(clone)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/query.py", line 262, in __len__
>     self._fetch_all()
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/query.py", line 1354, in _fetch_all
>     self._result_cache = list(self._iterable_class(self))
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/query.py", line 51, in __iter__
>     results = compiler.execute_sql(chunked_fetch=self.chunked_fetch,
> chunk_size=self.chunk_size)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/sql/compiler.py", line 1234, in execute_sql
>     return list(result)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/sql/compiler.py", line 1678, in cursor_iter
>     for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/models/sql/compiler.py", line 1678, in <lambda>
>     for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/utils.py", line 97, in inner
>     return func(*args, **kwargs)
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/utils.py", line 90, in __exit__
>     raise dj_exc_value.with_traceback(traceback) from exc_value
>   File "/home/company/.venv/project/lib/python3.8/site-
> packages/django/db/utils.py", line 97, in inner
>     return func(*args, **kwargs)
>
> Exception Type: ProgrammingError at /accounts/login/
> Exception Value: no results to fetch
> Request information:
> USER: AnonymousUser
> }}}
>
> This is the middleware it sits in:
> {{{
> from dist.terminals.apps import SESSION_SERIAL_NUMBER
>
> class UserSerialNumberMiddleware:
>     def __init__(self, get_response):
>         self.get_response = get_response
>         # One-time configuration and initialization.
>
>     def __call__(self, request):
>         user = request.user
>         user._serial_number = request.session.get(SESSION_SERIAL_NUMBER,
> '')
>         response = self.get_response(request)
>         return response
> }}}

New description:

 In my project, there are some methods on a custom user model that require
 the `request` to calculate certain values. This simple middleware does the
 trick:

 {{{
 class AttachRequestToUserMiddleware:
     """Adds the request to the user object, so session information can be
 looked
     up by the custom user model.

     Must come after
 django.contrib.auth.middleware.AuthenticationMiddleware which adds the
 user"""

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

     def __call__(self, request):
         request.user.request = request
         return self.get_response(request)
 }}}

 In production, when there are multiple workers running parrallel by uWSGI,
 if one has `SESSION_SAVE_EVERY_REQUEST = True`, then if one makes async
 requests from JavaScript (e.g. say a Service Worker caching pages on
 install), then the way it saves/retrieves sessions on every request fails
 in many spectacular ways.

 Here are some example trace backs:
 {{{
 Django Version: 4.0.1
 Python Version: 3.8.5

 Exception Type: ProgrammingError
 Exception Value:
 no results to fetch
 Exception Location:     /home/michael/.venv/project/lib/python3.8/site-
 packages/django/db/utils.py, line 97, in inner
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/db/utils.py, line 97, in inner
                 return func(*args, **kwargs)


 Exception Type: RuntimeError
 Exception Value:
 generator raised StopIteration
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/auth/__init__.py, line 60, in
 _get_user_session_key
 return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])

 Exception Type: MultipleObjectsReturned
 Exception Value:
 get() returned more than one Session -- it returned 2!
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/sessions/backends/base.py, line 180, in
 _get_session
             return self._session_cache
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/core/handlers/exception.py, line 47, in inner
                 response = get_response(request)
 ./core/accounts/middleware.py, line 33, in __call__
         request.user.request = request
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/utils/functional.py, line 278, in __setattr__
                 self._setup()
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/utils/functional.py, line 384, in _setup
         self._wrapped = self._setupfunc()
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/auth/middleware.py, line 25, in <lambda>
         request.user = SimpleLazyObject(lambda: get_user(request))
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/auth/middleware.py, line 11, in get_user
         request._cached_user = auth.get_user(request)
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/auth/__init__.py, line 177, in get_user
         user_id = _get_user_session_key(request)
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/auth/__init__.py, line 60, in
 _get_user_session_key
     return
 get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/sessions/backends/base.py, line 50, in __getitem__
         return self._session[key]
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/sessions/backends/base.py, line 185, in
 _get_session
                 self._session_cache = self.load()
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/sessions/backends/db.py, line 43, in load
         s = self._get_session_from_db()
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/contrib/sessions/backends/db.py, line 32, in
 _get_session_from_db
             return self.model.objects.get(
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/db/models/manager.py, line 85, in manager_method
                 return getattr(self.get_queryset(), name)(*args, **kwargs)
 /home/michael/.venv/project/lib/python3.8/site-
 packages/django/db/models/query.py, line 443, in get
         raise self.model.MultipleObjectsReturned(
 }}}

--

Comment (by Michael):

 Replying to [comment:1 Tim Graham]:
 > See TicketClosingReasons/UseSupportChannels for ways to get help if you
 cannot debug the issue yourself. Please only use this ticket tracker for
 reporting confirmed bugs. Thanks!

 Hi, I have done some more digging with some help:
 https://forum.djangoproject.com/t/should-it-be-impossible-for-a-session-
 dict-to-return-more-than-one-value-for-a-key/12298/13

 I think the issue is this: if it receives multiple requests at the same
 time (I have a service worker that requests many pages at installation so
 it can cache them), and if `SESSION_SAVE_EVERY_REQUEST = True`, then the
 way it handles session updates it just falls over in many spectacular
 ways.
 My for now my fix is to set SESSION_COOKIE_AGE to over a year, and have
 `SESSION_SAVE_EVERY_REQUEST = False`

-- 
Ticket URL: <https://code.djangoproject.com/ticket/33533#comment:3>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107017f8f4b6d1d-af60c0a5-ba9b-42c6-a05d-eaf50fb98df0-000000%40eu-central-1.amazonses.com.

Reply via email to