Repository: django/django
Issue category: Feature Request

Issue:
Add 307 and 308 redirect response codes to django.shortcuts.redirect
Description
	 
Other than 301 and 302 response codes for redirects, there is also:
​https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
​https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308
Currently, Django is unaware of these.
Proposal:
Add two new HTTP response classes for 307 and 308.
Enhance django.shortcuts.redirect with a new keyword argument preserve_method that, in combination with the existing permanent, decides which of the four redirect classes to use.
Add 307 and 308 redirect response codes to django.shortcuts.redirect
Description
	 
Other than 301 and 302 response codes for redirects, there is also:
​https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
​https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308
Currently, Django is unaware of these.
Proposal:
Add two new HTTP response classes for 307 and 308.
Enhance django.shortcuts.redirect with a new keyword argument preserve_method that, in combination with the existing permanent, decides which of the four redirect classes to use.


Hints:
['\u200bhttps://github.com/django/django/pull/18616 Proof of concept until the ticket itself is approved. After that tests and documentation will be updated.', 1727144938.0]
['\u200bhttps://github.com/django/django/pull/18616 Proof of concept until the ticket itself is approved. After that tests and documentation will be updated.', 1727144938.0]

Patch:
diff --git a/django/http/response.py b/django/http/response.py
index 1dbaf46adda4..4a0ea6701375 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -627,10 +627,12 @@ def set_headers(self, filelike):
 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 @@ def __repr__(self):
 
 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):
diff --git a/django/shortcuts.py b/django/shortcuts.py
index b8b5be1f5f54..6274631dbad7 100644
--- a/django/shortcuts.py
+++ b/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):
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index afebd00d8b89..26fcb5fa0824 100644
--- a/docs/ref/request-response.txt
+++ b/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
 
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
index 88a1daa45dc7..0ee4868246e1 100644
--- a/docs/releases/5.2.txt
+++ b/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
 ~~~~~~~~
 
diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt
index 171cfc3c9353..308eae085522 100644
--- a/docs/topics/http/shortcuts.txt
+++ b/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()``
 =======================
 


Test patch:
diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py
index 3774ff2d6727..f85d33e82338 100644
--- a/tests/httpwrappers/tests.py
+++ b/tests/httpwrappers/tests.py
@@ -566,6 +566,27 @@ def test_redirect_lazy(self):
         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 = (
diff --git a/tests/shortcuts/tests.py b/tests/shortcuts/tests.py
index 8e9c13d20620..b80b8f595139 100644
--- a/tests/shortcuts/tests.py
+++ b/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 @@ def test_render_with_using(self):
         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)