Opened 10 months ago
Closed 10 months ago
#36094 closed Bug (invalid)
A PicklingError occurs when using Django channels together with a logging SocketHandler, if an HTTP error status is returned.
| Reported by: | Vinay Sajip | Owned by: | |
|---|---|---|---|
| Component: | Core (Other) | Version: | 5.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Assume that the logging configuration includes a logging.handlers.SocketHandler. When a view returns an HTTP error status such as 400, the behavior is different according to whether or not channels are in use. When channels are not in use, you get this printed on the console:
Bad Request: /fail/400 [13/Jan/2025 21:39:04] "GET /fail/400 HTTP/1.1" 400 26314
Well and good. However, if channels are in use, you get this:
Bad Request: /fail/400
--- Logging error ---
Traceback (most recent call last):
File "/usr/lib/python3.10/logging/handlers.py", line 677, in emit
s = self.makePickle(record)
File "/usr/lib/python3.10/logging/handlers.py", line 649, in makePickle
s = pickle.dumps(d, 1)
File "/home/vinay/.local/share/virtualenvs/django-base/lib/python3.10/site-packages/django/urls/resolvers.py", line 105, in __reduce_ex__
raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
_pickle.PicklingError: Cannot pickle ResolverMatch.
Call stack:
File "/usr/lib/python3.10/threading.py", line 973, in _bootstrap
self._bootstrap_inner()
File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
self.run()
File "/usr/lib/python3.10/threading.py", line 953, in run
self._target(*self._args, **self._kwargs)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 83, in _worker
work_item.run()
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/home/vinay/.local/share/virtualenvs/django-base/lib/python3.10/site-packages/asgiref/sync.py", line 522, in thread_handler
return func(*args, **kwargs)
File "/home/vinay/.local/share/virtualenvs/django-base/lib/python3.10/site-packages/django/utils/log.py", line 248, in log_response
getattr(logger, level)(
Message: '%s: %s'
Arguments: ('Bad Request', '/fail/400')
HTTP GET /fail/400 400 [0.24, 127.0.0.1:49610]
The reason appears to be due to this code in django.utils.log.py, in function log_response:
getattr(logger, level)(
message,
*args,
extra={
"status_code": response.status_code,
"request": request,
},
exc_info=exception,
)
The difference between the channels case and the other is that in the channels case, the extra dictionary contains the request key points to an instance of django.core.handlers.asgi.ASGIRequest, whereas in the non-channels case the request key points to an instance of django.core.handlers.wsgi.WSGIRequest. In the WSGI request handling path, the resolver_match attribute of the request is not changed after being initialized to None, whereas in the ASGI request handling path, the resolver_match attribute is set to a ResolverMatch instance, which is not pickleable, and leads to the above traceback.
Furthermore, when looking at this problem, the ASGIRequest.__init__ sets the resolver_match attribute to None twice. Either one of those is redundant, or a comment is needed to say why the two set statements are present.