summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Dufresne <jon.dufresne@gmail.com>2020-07-07 16:23:54 -0700
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-09-07 06:54:19 +0200
commit11ebc6479ffda87376b60c9475d33d8120f86368 (patch)
treeccc77bccff2f90a3b16b887f975e08cbdbcf5419
parent83dea65ed649b5e80111017cd5c476c636872b2d (diff)
Fixed #31791 -- Made technical 404 debug page display the tried URL patterns for Http404.
-rw-r--r--django/urls/resolvers.py20
-rw-r--r--django/views/debug.py5
-rw-r--r--django/views/templates/technical_404.html7
-rw-r--r--docs/ref/urlresolvers.txt7
-rw-r--r--tests/view_tests/tests/test_debug.py30
5 files changed, 56 insertions, 13 deletions
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 5e3e6dea16..2a9dc11f28 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -30,12 +30,13 @@ from .utils import get_callable
class ResolverMatch:
- def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None, route=None):
+ def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None, route=None, tried=None):
self.func = func
self.args = args
self.kwargs = kwargs
self.url_name = url_name
self.route = route
+ self.tried = tried
# If a URLRegexResolver doesn't have a namespace or app_name, it passes
# in an empty value.
@@ -526,6 +527,13 @@ class URLResolver:
return self._app_dict[language_code]
@staticmethod
+ def _extend_tried(tried, pattern, sub_tried=None):
+ if sub_tried is None:
+ tried.append([pattern])
+ else:
+ tried.extend([pattern, *t] for t in sub_tried)
+
+ @staticmethod
def _join_route(route1, route2):
"""Join two routes, without the starting ^ in the second route."""
if not route1:
@@ -549,11 +557,7 @@ class URLResolver:
try:
sub_match = pattern.resolve(new_path)
except Resolver404 as e:
- sub_tried = e.args[0].get('tried')
- if sub_tried is not None:
- tried.extend([pattern] + t for t in sub_tried)
- else:
- tried.append([pattern])
+ self._extend_tried(tried, pattern, e.args[0].get('tried'))
else:
if sub_match:
# Merge captured arguments in match with submatch
@@ -566,6 +570,7 @@ class URLResolver:
if not sub_match_dict:
sub_match_args = args + sub_match.args
current_route = '' if isinstance(pattern, URLPattern) else str(pattern.pattern)
+ self._extend_tried(tried, pattern, sub_match.tried)
return ResolverMatch(
sub_match.func,
sub_match_args,
@@ -574,8 +579,9 @@ class URLResolver:
[self.app_name] + sub_match.app_names,
[self.namespace] + sub_match.namespaces,
self._join_route(current_route, sub_match.route),
+ tried,
)
- tried.append([pattern])
+ self._extend_tried(tried, pattern)
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path': path})
diff --git a/django/views/debug.py b/django/views/debug.py
index e45ef01ace..7a89f7bd15 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -481,8 +481,10 @@ def technical_404_response(request, exception):
try:
tried = exception.args[0]['tried']
except (IndexError, TypeError, KeyError):
- tried = []
+ resolved = True
+ tried = request.resolver_match.tried if request.resolver_match else None
else:
+ resolved = False
if (not tried or ( # empty URLconf
request.path == '/' and
len(tried) == 1 and # default URLconf
@@ -520,6 +522,7 @@ def technical_404_response(request, exception):
'root_urlconf': settings.ROOT_URLCONF,
'request_path': error_url,
'urlpatterns': tried,
+ 'resolved': resolved,
'reason': str(exception),
'request': request,
'settings': reporter_filter.get_safe_settings(),
diff --git a/django/views/templates/technical_404.html b/django/views/templates/technical_404.html
index 694309aa13..077bb20964 100644
--- a/django/views/templates/technical_404.html
+++ b/django/views/templates/technical_404.html
@@ -60,8 +60,11 @@
</ol>
<p>
{% if request_path %}
- The current path, <code>{{ request_path }}</code>,{% else %}
- The empty path{% endif %} didn’t match any of these.
+ The current path, <code>{{ request_path }}</code>,
+ {% else %}
+ The empty path
+ {% endif %}
+ {% if resolved %}matched the last one.{% else %}didn’t match any of these.{% endif %}
</p>
{% else %}
<p>{{ reason }}</p>
diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt
index 745ee9d6fd..b9af97f790 100644
--- a/docs/ref/urlresolvers.txt
+++ b/docs/ref/urlresolvers.txt
@@ -137,6 +137,13 @@ If the URL does not resolve, the function raises a
For example, if ``path('users/<id>/', ...)`` is the matching pattern,
``route`` will contain ``'users/<id>/'``.
+ .. attribute:: ResolverMatch.tried
+
+ .. versionadded:: 3.2
+
+ The list of URL patterns tried before the URL either matched one or
+ exhausted available patterns.
+
.. attribute:: ResolverMatch.app_name
The application namespace for the URL pattern that matches the
diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py
index 6e839b44f5..6d1b3e6f48 100644
--- a/tests/view_tests/tests/test_debug.py
+++ b/tests/view_tests/tests/test_debug.py
@@ -113,13 +113,25 @@ class DebugViewTests(SimpleTestCase):
def test_404(self):
response = self.client.get('/raises404/')
self.assertEqual(response.status_code, 404)
- self.assertContains(response, '<code>not-in-urls</code>, didn’t match', status_code=404)
+ self.assertContains(
+ response,
+ '<p>The current path, <code>not-in-urls</code>, didn’t match any '
+ 'of these.</p>',
+ status_code=404,
+ html=True,
+ )
def test_404_not_in_urls(self):
response = self.client.get('/not-in-urls')
self.assertNotContains(response, "Raised by:", status_code=404)
self.assertContains(response, "Django tried these URL patterns", status_code=404)
- self.assertContains(response, '<code>not-in-urls</code>, didn’t match', status_code=404)
+ self.assertContains(
+ response,
+ '<p>The current path, <code>not-in-urls</code>, didn’t match any '
+ 'of these.</p>',
+ status_code=404,
+ html=True,
+ )
# Pattern and view name of a RegexURLPattern appear.
self.assertContains(response, r"^regex-post/(?P&lt;pk&gt;[0-9]+)/$", status_code=404)
self.assertContains(response, "[name='regex-post']", status_code=404)
@@ -130,12 +142,24 @@ class DebugViewTests(SimpleTestCase):
@override_settings(ROOT_URLCONF=WithoutEmptyPathUrls)
def test_404_empty_path_not_in_urls(self):
response = self.client.get('/')
- self.assertContains(response, 'The empty path didn’t match any of these.', status_code=404)
+ self.assertContains(
+ response,
+ '<p>The empty path didn’t match any of these.</p>',
+ status_code=404,
+ html=True,
+ )
def test_technical_404(self):
response = self.client.get('/technical404/')
self.assertContains(response, "Raised by:", status_code=404)
self.assertContains(response, "view_tests.views.technical404", status_code=404)
+ self.assertContains(
+ response,
+ '<p>The current path, <code>technical404/</code>, matched the '
+ 'last one.</p>',
+ status_code=404,
+ html=True,
+ )
def test_classbased_technical_404(self):
response = self.client.get('/classbased404/')