#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 django-updates+unsubscr...@googlegroups.com.
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