0
0
mirror of https://github.com/django/django.git synced 2024-11-21 19:09:18 +01:00

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

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
Lorenzo Peña 2024-11-14 19:53:49 +01:00 committed by GitHub
parent 8590d05d44
commit 91c879eda5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 114 additions and 10 deletions

View File

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

View File

@ -26,7 +26,7 @@ def render(
return HttpResponse(content, content_type, status) 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 Return an HttpResponseRedirect to the appropriate URL for the arguments
passed. passed.
@ -40,13 +40,17 @@ def redirect(to, *args, permanent=False, **kwargs):
* A URL, which will be used as-is for the redirect location. * A URL, which will be used as-is for the redirect location.
Issues a temporary redirect by default; pass permanent=True to issue a Issues a temporary redirect by default. Set permanent=True to issue a
permanent redirect. 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 = ( redirect_class = (
HttpResponsePermanentRedirect if permanent else HttpResponseRedirect 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): def _get_queryset(klass):

View File

@ -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. ``'https://www.yahoo.com/search/'``), an absolute path with no domain
(e.g. ``'/search/'``), or even a relative path (e.g. ``'search/'``). In that (e.g. ``'/search/'``), or even a relative path (e.g. ``'search/'``). In that
last case, the client browser will reconstruct the full URL itself last case, the client browser will reconstruct the full URL itself
according to the current path. See :class:`HttpResponse` for other optional according to the current path.
constructor arguments. Note that this returns an HTTP status code 302.
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 .. attribute:: HttpResponseRedirect.url
This read-only attribute represents the URL the response will redirect This read-only attribute represents the URL the response will redirect
to (equivalent to the ``Location`` response header). to (equivalent to the ``Location`` response header).
.. versionchanged:: 5.2
The ``preserve_request`` argument was added.
.. class:: HttpResponsePermanentRedirect .. class:: HttpResponsePermanentRedirect
Like :class:`HttpResponseRedirect`, but it returns a permanent redirect Like :class:`HttpResponseRedirect`, but it returns a permanent redirect
(HTTP status code 301) instead of a "found" redirect (status code 302). (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 .. class:: HttpResponseNotModified

View File

@ -294,6 +294,16 @@ Requests and Responses
* The new :meth:`.HttpRequest.get_preferred_type` method can be used to query * The new :meth:`.HttpRequest.get_preferred_type` method can be used to query
the preferred media type the client accepts. 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 Security
~~~~~~~~ ~~~~~~~~

View File

@ -91,7 +91,7 @@ This example is equivalent to::
``redirect()`` ``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 Returns an :class:`~django.http.HttpResponseRedirect` to the appropriate URL
for the arguments passed. 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 * An absolute or relative URL, which will be used as-is for the redirect
location. location.
By default issues a temporary redirect; pass ``permanent=True`` to issue a By default, a temporary redirect is issued with a 302 status code. If
permanent redirect. ``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 Examples
-------- --------
@ -158,6 +177,17 @@ will be returned::
obj = MyModel.objects.get(...) obj = MyModel.objects.get(...)
return redirect(obj, permanent=True) 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()`` ``get_object_or_404()``
======================= =======================

View File

@ -566,6 +566,27 @@ class HttpResponseSubclassesTests(SimpleTestCase):
r = HttpResponseRedirect(lazystr("/redirected/")) r = HttpResponseRedirect(lazystr("/redirected/"))
self.assertEqual(r.url, "/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): def test_redirect_repr(self):
response = HttpResponseRedirect("/redirected/") response = HttpResponseRedirect("/redirected/")
expected = ( expected = (

View File

@ -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 import SimpleTestCase, override_settings
from django.test.utils import require_jinja2 from django.test.utils import require_jinja2
@ -35,3 +37,22 @@ class RenderTests(SimpleTestCase):
self.assertEqual(response.content, b"DTL\n") self.assertEqual(response.content, b"DTL\n")
response = self.client.get("/render/using/?using=jinja2") response = self.client.get("/render/using/?using=jinja2")
self.assertEqual(response.content, b"Jinja2\n") 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)