summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOlivier Tabone <olivier.tabone@ripplemotion.fr>2023-08-22 18:32:57 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-08-23 07:19:48 +0200
commita9e0f3d3014461c2199123721899162c0876959d (patch)
tree743b9ecf2b1d2e99898366b6446aeaa29d661a83
parent428023e2677aeb80d86b19f90b0c6b82c7cb666d (diff)
Refs #34757 -- Moved HTTP redirect logic to django.test.client.ClientMixin.
-rw-r--r--django/test/client.py135
1 files changed, 77 insertions, 58 deletions
diff --git a/django/test/client.py b/django/test/client.py
index 16d373ef65..a0e6c66e06 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -45,6 +45,16 @@ CONTENT_TYPE_RE = _lazy_re_compile(r".*; charset=([\w-]+);?")
# Structured suffix spec: https://tools.ietf.org/html/rfc6838#section-4.2.8
JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
+REDIRECT_STATUS_CODES = frozenset(
+ [
+ HTTPStatus.MOVED_PERMANENTLY,
+ HTTPStatus.FOUND,
+ HTTPStatus.SEE_OTHER,
+ HTTPStatus.TEMPORARY_REDIRECT,
+ HTTPStatus.PERMANENT_REDIRECT,
+ ]
+)
+
class RedirectCycleError(Exception):
"""The test client has been asked to follow a redirect loop."""
@@ -881,6 +891,69 @@ class ClientMixin:
)
return response._json
+ def _follow_redirect(
+ self, response, *, data="", content_type="", headers=None, **extra
+ ):
+ """Follow a single redirect contained in response using GET."""
+ response_url = response.url
+ redirect_chain = response.redirect_chain
+ redirect_chain.append((response_url, response.status_code))
+
+ url = urlsplit(response_url)
+ if url.scheme:
+ extra["wsgi.url_scheme"] = url.scheme
+ if url.hostname:
+ extra["SERVER_NAME"] = url.hostname
+ if url.port:
+ extra["SERVER_PORT"] = str(url.port)
+
+ path = url.path
+ # RFC 3986 Section 6.2.3: Empty path should be normalized to "/".
+ if not path and url.netloc:
+ path = "/"
+ # Prepend the request path to handle relative path redirects
+ if not path.startswith("/"):
+ path = urljoin(response.request["PATH_INFO"], path)
+
+ if response.status_code in (
+ HTTPStatus.TEMPORARY_REDIRECT,
+ HTTPStatus.PERMANENT_REDIRECT,
+ ):
+ # Preserve request method and query string (if needed)
+ # post-redirect for 307/308 responses.
+ request_method = response.request["REQUEST_METHOD"].lower()
+ if request_method not in ("get", "head"):
+ extra["QUERY_STRING"] = url.query
+ request_method = getattr(self, request_method)
+ else:
+ request_method = self.get
+ data = QueryDict(url.query)
+ content_type = None
+
+ return request_method(
+ path,
+ data=data,
+ content_type=content_type,
+ follow=False,
+ headers=headers,
+ **extra,
+ )
+
+ def _ensure_redirects_not_cyclic(self, response):
+ """
+ Raise a RedirectCycleError if response contains too many redirects.
+ """
+ redirect_chain = response.redirect_chain
+ if redirect_chain[-1] in redirect_chain[:-1]:
+ # Check that we're not redirecting to somewhere we've already been
+ # to, to prevent loops.
+ raise RedirectCycleError("Redirect loop detected.", last_response=response)
+ if len(redirect_chain) > 20:
+ # Such a lengthy chain likely also means a loop, but one with a
+ # growing path, changing view, or changing query argument. 20 is
+ # the value of "network.http.redirection-limit" from Firefox.
+ raise RedirectCycleError("Too many redirects.", last_response=response)
+
class Client(ClientMixin, RequestFactory):
"""
@@ -1179,71 +1252,17 @@ class Client(ClientMixin, RequestFactory):
Follow any redirects by requesting responses from the server using GET.
"""
response.redirect_chain = []
- redirect_status_codes = (
- HTTPStatus.MOVED_PERMANENTLY,
- HTTPStatus.FOUND,
- HTTPStatus.SEE_OTHER,
- HTTPStatus.TEMPORARY_REDIRECT,
- HTTPStatus.PERMANENT_REDIRECT,
- )
- while response.status_code in redirect_status_codes:
- response_url = response.url
+ while response.status_code in REDIRECT_STATUS_CODES:
redirect_chain = response.redirect_chain
- redirect_chain.append((response_url, response.status_code))
-
- url = urlsplit(response_url)
- if url.scheme:
- extra["wsgi.url_scheme"] = url.scheme
- if url.hostname:
- extra["SERVER_NAME"] = url.hostname
- if url.port:
- extra["SERVER_PORT"] = str(url.port)
-
- path = url.path
- # RFC 3986 Section 6.2.3: Empty path should be normalized to "/".
- if not path and url.netloc:
- path = "/"
- # Prepend the request path to handle relative path redirects
- if not path.startswith("/"):
- path = urljoin(response.request["PATH_INFO"], path)
-
- if response.status_code in (
- HTTPStatus.TEMPORARY_REDIRECT,
- HTTPStatus.PERMANENT_REDIRECT,
- ):
- # Preserve request method and query string (if needed)
- # post-redirect for 307/308 responses.
- request_method = response.request["REQUEST_METHOD"].lower()
- if request_method not in ("get", "head"):
- extra["QUERY_STRING"] = url.query
- request_method = getattr(self, request_method)
- else:
- request_method = self.get
- data = QueryDict(url.query)
- content_type = None
-
- response = request_method(
- path,
+ response = self._follow_redirect(
+ response,
data=data,
content_type=content_type,
- follow=False,
headers=headers,
**extra,
)
response.redirect_chain = redirect_chain
-
- if redirect_chain[-1] in redirect_chain[:-1]:
- # Check that we're not redirecting to somewhere we've already
- # been to, to prevent loops.
- raise RedirectCycleError(
- "Redirect loop detected.", last_response=response
- )
- if len(redirect_chain) > 20:
- # Such a lengthy chain likely also means a loop, but one with
- # a growing path, changing view, or changing query argument;
- # 20 is the value of "network.http.redirection-limit" from Firefox.
- raise RedirectCycleError("Too many redirects.", last_response=response)
-
+ self._ensure_redirects_not_cyclic(response)
return response