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 = [