Skip to content

Commit

Permalink
Refs #34757 -- Moved HTTP redirect logic to django.test.client.Client…
Browse files Browse the repository at this point in the history
…Mixin.
  • Loading branch information
olibook authored and felixxm committed Aug 23, 2023
1 parent 428023e commit a9e0f3d
Showing 1 changed file with 77 additions and 58 deletions.
135 changes: 77 additions & 58 deletions django/test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
# 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."""
Expand Down Expand Up @@ -881,6 +891,69 @@ def _parse_json(self, response, **extra):
)
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):
"""
Expand Down Expand Up @@ -1179,71 +1252,17 @@ def _handle_redirects(
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


Expand Down

0 comments on commit a9e0f3d

Please sign in to comment.