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)