diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 1c43953717..8876f47dea 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -64,6 +64,9 @@ class LimitedStream: class WSGIRequest(HttpRequest): + non_picklable_attrs = HttpRequest.non_picklable_attrs | frozenset(["environ"]) + meta_non_picklable_attrs = frozenset(["wsgi.errors", "wsgi.input"]) + def __init__(self, environ): script_name = get_script_name(environ) # If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a @@ -89,6 +92,13 @@ class WSGIRequest(HttpRequest): self._read_started = False self.resolver_match = None + def __getstate__(self): + state = super().__getstate__() + for attr in self.meta_non_picklable_attrs: + if attr in state["META"]: + del state["META"][attr] + return state + def _get_scheme(self): return self.environ.get("wsgi.url_scheme") diff --git a/django/http/request.py b/django/http/request.py index d65adce756..815544368b 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -51,6 +51,8 @@ class HttpRequest: _encoding = None _upload_handlers = [] + non_picklable_attrs = frozenset(["resolver_match", "_stream"]) + def __init__(self): # WARNING: The `WSGIRequest` subclass doesn't call `super`. # Any variable assignment made here should also happen in @@ -78,6 +80,21 @@ class HttpRequest: self.get_full_path(), ) + def __getstate__(self): + obj_dict = self.__dict__.copy() + for attr in self.non_picklable_attrs: + if attr in obj_dict: + del obj_dict[attr] + return obj_dict + + def __deepcopy__(self, memo): + obj = copy.copy(self) + for attr in self.non_picklable_attrs: + if hasattr(self, attr): + setattr(obj, attr, copy.deepcopy(getattr(self, attr), memo)) + memo[id(self)] = obj + return obj + @cached_property def headers(self): return HttpHeaders(self.META) diff --git a/django/http/response.py b/django/http/response.py index 7a0dd688f7..7c0db55a5d 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -370,12 +370,10 @@ class HttpResponse(HttpResponseBase): [ "resolver_match", # Non-picklable attributes added by test clients. - "asgi_request", "client", "context", "json", "templates", - "wsgi_request", ] ) diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 6d2d7d777a..d8068583a7 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -1,3 +1,4 @@ +import pickle from io import BytesIO from itertools import chain from urllib.parse import urlencode @@ -669,6 +670,20 @@ class RequestsTests(SimpleTestCase): with self.assertRaises(UnreadablePostError): request.FILES + def test_pickling_request(self): + request = HttpRequest() + request.method = "GET" + request.path = "/testpath/" + request.META = { + "QUERY_STRING": ";some=query&+query=string", + "SERVER_NAME": "example.com", + "SERVER_PORT": 80, + } + request.COOKIES = {"post-key": "post-value"} + dump = pickle.dumps(request) + request_from_pickle = pickle.loads(dump) + self.assertEqual(repr(request), repr(request_from_pickle)) + class HostValidationTests(SimpleTestCase): poisoned_hosts = [