mirror of https://github.com/django/django.git
Refs #34757 -- Moved HTTP redirect logic to django.test.client.ClientMixin.
This commit is contained in:
parent
428023e267
commit
a9e0f3d301
|
@ -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
|
# Structured suffix spec: https://tools.ietf.org/html/rfc6838#section-4.2.8
|
||||||
JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
|
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):
|
class RedirectCycleError(Exception):
|
||||||
"""The test client has been asked to follow a redirect loop."""
|
"""The test client has been asked to follow a redirect loop."""
|
||||||
|
@ -881,6 +891,69 @@ class ClientMixin:
|
||||||
)
|
)
|
||||||
return response._json
|
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):
|
class Client(ClientMixin, RequestFactory):
|
||||||
"""
|
"""
|
||||||
|
@ -1179,71 +1252,17 @@ class Client(ClientMixin, RequestFactory):
|
||||||
Follow any redirects by requesting responses from the server using GET.
|
Follow any redirects by requesting responses from the server using GET.
|
||||||
"""
|
"""
|
||||||
response.redirect_chain = []
|
response.redirect_chain = []
|
||||||
redirect_status_codes = (
|
while response.status_code in 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
|
|
||||||
redirect_chain = response.redirect_chain
|
redirect_chain = response.redirect_chain
|
||||||
redirect_chain.append((response_url, response.status_code))
|
response = self._follow_redirect(
|
||||||
|
response,
|
||||||
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,
|
|
||||||
data=data,
|
data=data,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
follow=False,
|
|
||||||
headers=headers,
|
headers=headers,
|
||||||
**extra,
|
**extra,
|
||||||
)
|
)
|
||||||
response.redirect_chain = redirect_chain
|
response.redirect_chain = redirect_chain
|
||||||
|
self._ensure_redirects_not_cyclic(response)
|
||||||
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)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue