瀏覽代碼

Fixed #35784 -- Added support for preserving the HTTP request method in HttpResponseRedirectBase.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Lorenzo Peña 4 月之前
父節點
當前提交
91c879eda5

+ 5 - 1
django/http/response.py

@@ -627,10 +627,12 @@ class FileResponse(StreamingHttpResponse):
 class HttpResponseRedirectBase(HttpResponse):
     allowed_schemes = ["http", "https", "ftp"]
 
-    def __init__(self, redirect_to, *args, **kwargs):
+    def __init__(self, redirect_to, preserve_request=False, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self["Location"] = iri_to_uri(redirect_to)
         parsed = urlsplit(str(redirect_to))
+        if preserve_request:
+            self.status_code = self.status_code_preserve_request
         if parsed.scheme and parsed.scheme not in self.allowed_schemes:
             raise DisallowedRedirect(
                 "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
@@ -652,10 +654,12 @@ class HttpResponseRedirectBase(HttpResponse):
 
 class HttpResponseRedirect(HttpResponseRedirectBase):
     status_code = 302
+    status_code_preserve_request = 307
 
 
 class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
     status_code = 301
+    status_code_preserve_request = 308
 
 
 class HttpResponseNotModified(HttpResponse):

+ 8 - 4
django/shortcuts.py

@@ -26,7 +26,7 @@ def render(
     return HttpResponse(content, content_type, status)
 
 
-def redirect(to, *args, permanent=False, **kwargs):
+def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
     """
     Return an HttpResponseRedirect to the appropriate URL for the arguments
     passed.
@@ -40,13 +40,17 @@ def redirect(to, *args, permanent=False, **kwargs):
 
         * A URL, which will be used as-is for the redirect location.
 
-    Issues a temporary redirect by default; pass permanent=True to issue a
-    permanent redirect.
+    Issues a temporary redirect by default. Set permanent=True to issue a
+    permanent redirect. Set preserve_request=True to instruct the user agent
+    to preserve the original HTTP method and body when following the redirect.
     """
     redirect_class = (
         HttpResponsePermanentRedirect if permanent else HttpResponseRedirect
     )
-    return redirect_class(resolve_url(to, *args, **kwargs))
+    return redirect_class(
+        resolve_url(to, *args, **kwargs),
+        preserve_request=preserve_request,
+    )
 
 
 def _get_queryset(klass):

+ 16 - 2
docs/ref/request-response.txt

@@ -1070,18 +1070,32 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
     (e.g. ``'https://www.yahoo.com/search/'``), an absolute path with no domain
     (e.g. ``'/search/'``), or even a relative path (e.g. ``'search/'``). In that
     last case, the client browser will reconstruct the full URL itself
-    according to the current path. See :class:`HttpResponse` for other optional
-    constructor arguments. Note that this returns an HTTP status code 302.
+    according to the current path.
+
+    The constructor accepts an optional ``preserve_request`` keyword argument
+    that defaults to ``False``, producing a response with a 302 status code. If
+    ``preserve_request`` is ``True``, the status code will be 307 instead.
+
+    See :class:`HttpResponse` for other optional constructor arguments.
 
     .. attribute:: HttpResponseRedirect.url
 
         This read-only attribute represents the URL the response will redirect
         to (equivalent to the ``Location`` response header).
 
+    .. versionchanged:: 5.2
+
+        The ``preserve_request`` argument was added.
+
 .. class:: HttpResponsePermanentRedirect
 
     Like :class:`HttpResponseRedirect`, but it returns a permanent redirect
     (HTTP status code 301) instead of a "found" redirect (status code 302).
+    When ``preserve_request=True``, the response's status code is 308.
+
+    .. versionchanged:: 5.2
+
+        The ``preserve_request`` argument was added.
 
 .. class:: HttpResponseNotModified
 

+ 10 - 0
docs/releases/5.2.txt

@@ -294,6 +294,16 @@ Requests and Responses
 * The new :meth:`.HttpRequest.get_preferred_type` method can be used to query
   the preferred media type the client accepts.
 
+* The new ``preserve_request`` argument for
+  :class:`~django.http.HttpResponseRedirect` and
+  :class:`~django.http.HttpResponsePermanentRedirect`
+  determines whether the HTTP status codes 302/307 or 301/308 are used,
+  respectively.
+
+* The new ``preserve_request`` argument for
+  :func:`~django.shortcuts.redirect` allows to instruct the user agent to reuse
+  the HTTP method and body during redirection using specific status codes.
+
 Security
 ~~~~~~~~
 

+ 33 - 3
docs/topics/http/shortcuts.txt

@@ -91,7 +91,7 @@ This example is equivalent to::
 ``redirect()``
 ==============
 
-.. function:: redirect(to, *args, permanent=False, **kwargs)
+.. function:: redirect(to, *args, permanent=False, preserve_request=False, **kwargs)
 
    Returns an :class:`~django.http.HttpResponseRedirect` to the appropriate URL
    for the arguments passed.
@@ -107,8 +107,27 @@ This example is equivalent to::
    * An absolute or relative URL, which will be used as-is for the redirect
      location.
 
-   By default issues a temporary redirect; pass ``permanent=True`` to issue a
-   permanent redirect.
+   By default, a temporary redirect is issued with a 302 status code. If
+   ``permanent=True``, a permanent redirect is issued with a 301 status code.
+
+   If ``preserve_request=True``, the response instructs the user agent to
+   preserve the method and body of the original request when issuing the
+   redirect. In this case, temporary redirects use a 307 status code, and
+   permanent redirects use a 308 status code. This is better illustrated in the
+   following table:
+
+   =========  ================ ================
+   permanent  preserve_request HTTP status code
+   =========  ================ ================
+   ``True``   ``False``        301
+   ``False``  ``False``        302
+   ``False``  ``True``         307
+   ``True``   ``True``         308
+   =========  ================ ================
+
+   .. versionchanged:: 5.2
+
+       The argument ``preserve_request`` was added.
 
 Examples
 --------
@@ -158,6 +177,17 @@ will be returned::
         obj = MyModel.objects.get(...)
         return redirect(obj, permanent=True)
 
+Additionally, the ``preserve_request`` argument can be used to preserve the
+original HTTP method::
+
+    def my_view(request):
+        # ...
+        obj = MyModel.objects.get(...)
+        if request.method in ("POST", "PUT"):
+            # Redirection preserves the original request method.
+            return redirect(obj, preserve_request=True)
+        # ...
+
 ``get_object_or_404()``
 =======================
 

+ 21 - 0
tests/httpwrappers/tests.py

@@ -566,6 +566,27 @@ class HttpResponseSubclassesTests(SimpleTestCase):
         r = HttpResponseRedirect(lazystr("/redirected/"))
         self.assertEqual(r.url, "/redirected/")
 
+    def test_redirect_modifiers(self):
+        cases = [
+            (HttpResponseRedirect, "Moved temporarily", False, 302),
+            (HttpResponseRedirect, "Moved temporarily preserve method", True, 307),
+            (HttpResponsePermanentRedirect, "Moved permanently", False, 301),
+            (
+                HttpResponsePermanentRedirect,
+                "Moved permanently preserve method",
+                True,
+                308,
+            ),
+        ]
+        for response_class, content, preserve_request, expected_status_code in cases:
+            with self.subTest(status_code=expected_status_code):
+                response = response_class(
+                    "/redirected/", content=content, preserve_request=preserve_request
+                )
+                self.assertEqual(response.status_code, expected_status_code)
+                self.assertEqual(response.content.decode(), content)
+                self.assertEqual(response.url, response.headers["Location"])
+
     def test_redirect_repr(self):
         response = HttpResponseRedirect("/redirected/")
         expected = (

+ 21 - 0
tests/shortcuts/tests.py

@@ -1,3 +1,5 @@
+from django.http.response import HttpResponseRedirectBase
+from django.shortcuts import redirect
 from django.test import SimpleTestCase, override_settings
 from django.test.utils import require_jinja2
 
@@ -35,3 +37,22 @@ class RenderTests(SimpleTestCase):
         self.assertEqual(response.content, b"DTL\n")
         response = self.client.get("/render/using/?using=jinja2")
         self.assertEqual(response.content, b"Jinja2\n")
+
+
+class RedirectTests(SimpleTestCase):
+    def test_redirect_response_status_code(self):
+        tests = [
+            (True, False, 301),
+            (False, False, 302),
+            (False, True, 307),
+            (True, True, 308),
+        ]
+        for permanent, preserve_request, expected_status_code in tests:
+            with self.subTest(permanent=permanent, preserve_request=preserve_request):
+                response = redirect(
+                    "/path/is/irrelevant/",
+                    permanent=permanent,
+                    preserve_request=preserve_request,
+                )
+                self.assertIsInstance(response, HttpResponseRedirectBase)
+                self.assertEqual(response.status_code, expected_status_code)