summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorenzo Peña <lorinkoz@gmail.com>2024-11-14 19:53:49 +0100
committerGitHub <noreply@github.com>2024-11-14 15:53:49 -0300
commit91c879eda595c12477bbfa6f51115e88b75ddf88 (patch)
tree544ce7be64975158b6d3f0d0a8ea693ab7188aca
parent8590d05d44a4f3df56d988229e43d66c37df79da (diff)
Fixed #35784 -- Added support for preserving the HTTP request method in HttpResponseRedirectBase.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
-rw-r--r--django/http/response.py6
-rw-r--r--django/shortcuts.py12
-rw-r--r--docs/ref/request-response.txt18
-rw-r--r--docs/releases/5.2.txt10
-rw-r--r--docs/topics/http/shortcuts.txt36
-rw-r--r--tests/httpwrappers/tests.py21
-rw-r--r--tests/shortcuts/tests.py21
7 files changed, 114 insertions, 10 deletions
diff --git a/django/http/response.py b/django/http/response.py
index 1dbaf46add..4a0ea67013 100644
--- a/django/http/response.py
+++ b/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):
diff --git a/django/shortcuts.py b/django/shortcuts.py
index b8b5be1f5f..6274631dba 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 afebd00d8b..26fcb5fa08 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 88a1daa45d..0ee4868246 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 171cfc3c93..308eae0855 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()``
=======================
diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py
index 3774ff2d67..f85d33e823 100644
--- a/tests/httpwrappers/tests.py
+++ b/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 = (
diff --git a/tests/shortcuts/tests.py b/tests/shortcuts/tests.py
index 8e9c13d206..b80b8f5951 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 @@ 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)