Browse Source

Fixed #31791 -- Made technical 404 debug page display the tried URL patterns for Http404.

Jon Dufresne 4 years ago
parent
commit
11ebc6479f

+ 13 - 7
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.
@@ -525,6 +526,13 @@ class URLResolver:
             self._populate()
         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."""
@@ -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})
 

+ 4 - 1
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(),

+ 5 - 2
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>

+ 7 - 0
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

+ 27 - 3
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/')