Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions django/core/handlers/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self, scope, body_file):
self.path_info = scope["path"].removeprefix(self.script_name)
else:
self.path_info = scope["path"]
self.path_info_is_empty = not bool(self.path_info)
# HTTP basics.
self.method = self.scope["method"].upper()
# Ensure query string is encoded correctly.
Expand Down
13 changes: 11 additions & 2 deletions django/core/handlers/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,24 @@ def __init__(self, environ):
script_name = get_script_name(environ)
# If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
# trailing slash), operate as if '/' was requested.
path_info = get_path_info(environ) or "/"
path_info = environ_path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g. accessing
# the SCRIPT_NAME URL without a trailing slash). We really need to
# operate as if they'd requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
self.path_info_is_empty = True
path_info = "/"
else:
self.path_info_is_empty = False
self.environ = environ
self.path_info = path_info
# be careful to only replace the first slash in the path because of
# http://test/something and http://test//something being different as
# stated in RFC 3986.
self.path = "%s/%s" % (script_name.rstrip("/"), path_info.replace("/", "", 1))
self.META = environ
self.META["PATH_INFO"] = path_info
self.META["PATH_INFO"] = environ_path_info
self.META["SCRIPT_NAME"] = script_name
self.method = environ["REQUEST_METHOD"].upper()
# Set content_type, content_params, and encoding.
Expand Down
1 change: 1 addition & 0 deletions django/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(self):

self.path = ""
self.path_info = ""
self.path_info_is_empty = True
self.method = None
self.resolver_match = None
self.content_type = None
Expand Down
33 changes: 20 additions & 13 deletions django/middleware/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,35 @@ def process_request(self, request):

# Check for a redirect based on settings.PREPEND_WWW
host = request.get_host()
must_prepend = settings.PREPEND_WWW and host and not host.startswith("www.")
redirect_url = f"{request.scheme}://www.{host}" if must_prepend else ""

if settings.PREPEND_WWW and host and not host.startswith("www."):
# Check if we also need to append a slash so we can do it all
# with a single redirect. (This check may be somewhat expensive,
# so we only do it if we already know we're sending a redirect,
# or in process_response if we get a 404.)
if self.should_redirect_with_slash(request):
path = self.get_full_path_with_slash(request)
else:
path = request.get_full_path()
# Check if a slash should be appended to the URL
should_redirect_with_slash = self.should_redirect_with_slash(request)

return self.response_redirect_class(f"{request.scheme}://www.{host}{path}")
# If a slash should be appended, use the full path with a slash.
# Otherwise, just get the full path without forcing a slash.
if should_redirect_with_slash:
path = self.get_full_path_with_slash(request)
else:
path = request.get_full_path()

# If it's needed to redirect either based on settings.PREPEND_WWW
# or to append a slash, do so.
if redirect_url or should_redirect_with_slash:
redirect_url = f"{redirect_url}{path}"
return self.response_redirect_class(redirect_url)

def should_redirect_with_slash(self, request):
"""
Return True if settings.APPEND_SLASH is True and appending a slash to
the request path turns an invalid path into a valid one.
"""
if settings.APPEND_SLASH and not request.path_info.endswith("/"):
path_info = "" if request.path_info_is_empty else request.path_info
if settings.APPEND_SLASH and not path_info.endswith("/"):
urlconf = getattr(request, "urlconf", None)
if not is_valid_path(request.path_info, urlconf):
match = is_valid_path("%s/" % request.path_info, urlconf)
if not is_valid_path(path_info, urlconf):
match = is_valid_path("%s/" % path_info, urlconf)
if match:
view = match.func
return getattr(view, "should_append_slash", True)
Expand Down
46 changes: 46 additions & 0 deletions tests/handlers/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,52 @@ def test_invalid_multipart_boundary(self):
# Expect "bad request" response
self.assertEqual(response.status_code, 400)

@override_settings(ROOT_URLCONF="handlers.urls")
def test_root_path_info_with_slash(self):
"""
If PATH_INFO is '/' and APPEND_SLASH is True, and a URL pattern
is defined for '^/$', then Django should render a response
from the corresponding view.
"""
environ = RequestFactory().get("/").environ
handler = WSGIHandler()
response = handler(environ, lambda *a, **k: None)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Index")

@override_settings(ROOT_URLCONF="handlers.urls")
def test_root_path_info_without_slash(self):
"""
If PATH_INFO is empty and APPEND_SLASH is True, and a url pattern
is defined for '^/$' but not for '^$', then CommonMiddleware should
issue a redirect.
"""
environ = RequestFactory().get("").environ
handler = WSGIHandler()
response = handler(environ, lambda *a, **k: None)
self.assertEqual(response.status_code, 301)
self.assertEqual(response.url, "/")

@override_settings(APPEND_SLASH=False, ROOT_URLCONF="handlers.urls")
def test_root_path_info_nonempty_script_name_no_append_slash(self):
environ = RequestFactory().get("").environ
environ["SCRIPT_NAME"] = "site-root"
environ["PATH_INFO"] = ""
handler = WSGIHandler()
response = handler(environ, lambda *a, **k: None)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Index")

@override_settings(APPEND_SLASH=True, ROOT_URLCONF="handlers.urls")
def test_root_path_info_nonempty_script_name_with_append_slash(self):
environ = RequestFactory().get("").environ
environ["SCRIPT_NAME"] = "site-root"
environ["PATH_INFO"] = ""
handler = WSGIHandler()
response = handler(environ, lambda *a, **k: None)
self.assertEqual(response.status_code, 301)
self.assertEqual(response.url, "site-root/")


@override_settings(ROOT_URLCONF="handlers.urls", MIDDLEWARE=[])
class TransactionsPerRequestTests(TransactionTestCase):
Expand Down
3 changes: 2 additions & 1 deletion tests/handlers/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.urls import path
from django.urls import path, re_path

from . import views

Expand All @@ -18,4 +18,5 @@
path("malformed_post/", views.malformed_post),
path("httpstatus_enum/", views.httpstatus_enum),
path("unawaited/", views.async_unawaited),
re_path("^$", views.index),
]
4 changes: 4 additions & 0 deletions tests/handlers/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from django.views.decorators.csrf import csrf_exempt


def index(request):
return HttpResponse(b"Index")


def regular(request):
return HttpResponse(b"regular content")

Expand Down
56 changes: 44 additions & 12 deletions tests/middleware/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,15 @@ def test_append_slash_leading_slashes(self):
"""
# Use 4 slashes because of RequestFactory behavior.
request = self.rf.get("////evil.com/security")
r = CommonMiddleware(get_response_404).process_request(request)
self.assertIsNone(r)
res = CommonMiddleware(get_response_404).process_request(request)
self.assertIsNone(res)
response = HttpResponseNotFound()
r = CommonMiddleware(get_response_404).process_response(request, response)
self.assertEqual(r.status_code, 301)
self.assertEqual(r.url, "/%2Fevil.com/security/")
r = CommonMiddleware(get_response_404)(request)
self.assertEqual(r.status_code, 301)
self.assertEqual(r.url, "/%2Fevil.com/security/")
res = CommonMiddleware(get_response_404).process_response(request, response)
self.assertEqual(res.status_code, 301)
self.assertEqual(res.url, "/%2Fevil.com/security/")
res = CommonMiddleware(get_response_404)(request)
self.assertEqual(res.status_code, 301)
self.assertEqual(res.url, "/%2Fevil.com/security/")

@override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
def test_prepend_www(self):
Expand Down Expand Up @@ -319,6 +319,38 @@ def test_prepend_www_append_slash_slashless_custom_urlconf(self):
self.assertEqual(r.status_code, 301)
self.assertEqual(r.url, "http://www.testserver/customurlconf/slash/")

@override_settings(APPEND_SLASH=True)
def test_empty_path_info_not_found_with_append_slash(self):
req = HttpRequest()
req.urlconf = "middleware.urls"
res = HttpResponseNotFound()
middleware_res = CommonMiddleware(get_response_empty).process_response(req, res)
self.assertEqual(middleware_res.status_code, 301)

@override_settings(APPEND_SLASH=False)
def test_empty_path_info_not_found_without_append_slash(self):
req = HttpRequest()
req.urlconf = "middleware.urls"
res = HttpResponseNotFound()
middleware_res = CommonMiddleware(get_response_empty).process_response(req, res)
self.assertEqual(middleware_res.status_code, 404)

@override_settings(APPEND_SLASH=True)
def test_empty_path_info_200_with_append_slash(self):
req = HttpRequest()
req.urlconf = "middleware.urls"
res = HttpResponse("content")
middleware_res = CommonMiddleware(get_response_empty).process_response(req, res)
self.assertEqual(middleware_res.status_code, 200)

@override_settings(APPEND_SLASH=False)
def test_empty_path_info_200_without_append_slash(self):
req = HttpRequest()
req.urlconf = "middleware.urls"
res = HttpResponse("content")
middleware_res = CommonMiddleware(get_response_empty).process_response(req, res)
self.assertEqual(middleware_res.status_code, 200)

# Tests for the Content-Length header

def test_content_length_header_added(self):
Expand Down Expand Up @@ -376,11 +408,11 @@ def test_non_ascii_query_string_does_not_crash(self):
"""Regression test for #15152"""
request = self.rf.get("/slash")
request.META["QUERY_STRING"] = "drink=café"
r = CommonMiddleware(get_response_empty).process_request(request)
self.assertIsNone(r)
res = CommonMiddleware(get_response_empty).process_request(request)
self.assertEqual(res.status_code, 301)
response = HttpResponseNotFound()
r = CommonMiddleware(get_response_empty).process_response(request, response)
self.assertEqual(r.status_code, 301)
res = CommonMiddleware(get_response_empty).process_response(request, response)
self.assertEqual(res.status_code, 301)

def test_response_redirect_class(self):
request = self.rf.get("/slash")
Expand Down
1 change: 1 addition & 0 deletions tests/middleware/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@
path("csp-override-enforced/", views.csp_override_enforced),
path("csp-override-report-only/", views.csp_override_report_only),
path("csp-500/", views.csp_500),
re_path(r"^$", views.empty_view),
]
Loading