diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index 302f68dd07..7272595355 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -1,9 +1,3 @@ from django.urls import include -from django.views import defaults -__all__ = ["handler400", "handler403", "handler404", "handler500", "include"] - -handler400 = defaults.bad_request -handler403 = defaults.permission_denied -handler404 = defaults.page_not_found -handler500 = defaults.server_error +__all__ = ["include"] diff --git a/django/core/checks/urls.py b/django/core/checks/urls.py index aef2bfebb0..171e39334c 100644 --- a/django/core/checks/urls.py +++ b/django/core/checks/urls.py @@ -131,7 +131,7 @@ def check_custom_error_handlers(app_configs, **kwargs): errors = [] # All handlers take (request, exception) arguments except handler500 # which takes (request). - for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]: + for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 2)]: try: handler = resolver.resolve_error_handler(status_code) except (ImportError, ViewDoesNotExist) as e: diff --git a/django/http/response.py b/django/http/response.py index 4a0ea67013..3379d63a06 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -211,6 +211,23 @@ class HttpResponseBase: def get(self, header, alternate=None): return self.headers.get(header, alternate) + @classmethod + def response_class_by_status_code(cls, status_code): + return { + 200: HttpResponse, + 301: HttpResponsePermanentRedirect, + 302: HttpResponseRedirect, + 304: HttpResponseNotModified, + 307: HttpResponseRedirect, + 308: HttpResponsePermanentRedirect, + 400: HttpResponseBadRequest, + 403: HttpResponseForbidden, + 404: HttpResponseNotFound, + 405: HttpResponseNotAllowed, + 410: HttpResponseGone, + 500: HttpResponseServerError, + }.get(status_code, cls) + def set_cookie( self, key, diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index c667d7f268..cd1d514a11 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -10,6 +10,7 @@ import functools import inspect import re import string +import warnings from importlib import import_module from pickle import PicklingError from urllib.parse import quote @@ -21,6 +22,7 @@ from django.core.checks import Error, Warning from django.core.checks.urls import check_resolver from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import MultiValueDict +from django.utils.deprecation import RemovedInDjango61Warning from django.utils.functional import cached_property from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes from django.utils.regex_helper import _lazy_re_compile, normalize @@ -728,15 +730,26 @@ class URLResolver: raise ImproperlyConfigured(msg.format(name=self.urlconf_name)) from e return patterns - def resolve_error_handler(self, view_type): - callback = getattr(self.urlconf_module, "handler%s" % view_type, None) - if not callback: + def resolve_error_handler(self, status_code): + # RemovedInDjango61Warning. + callback = getattr(self.urlconf_module, f"handler{status_code}", None) + if callback: + warnings.warn( + "handler custom error handlers are deprecated, please " + "replace them by a generic `error_handler` view function.", + RemovedInDjango61Warning, + ) + return get_callable(callback) + error_view = getattr(self.urlconf_module, "error_handler", None) + if error_view: + error_view.status_code = status_code + else: # No handler specified in file; use lazy import, since - # django.conf.urls imports this file. - from django.conf import urls + # django.views.defaults imports this file. + from django.views.defaults import DefaultErrorView - callback = getattr(urls, "handler%s" % view_type) - return get_callable(callback) + error_view = DefaultErrorView.as_view(status_code=status_code) + return error_view def reverse(self, lookup_view, *args, **kwargs): return self._reverse_with_prefix(lookup_view, "", *args, **kwargs) diff --git a/django/views/defaults.py b/django/views/defaults.py index 8f56a8fb89..33bf07a220 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,13 +1,17 @@ from urllib.parse import quote from django.http import ( + HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError, ) from django.template import Context, Engine, TemplateDoesNotExist, loader +from django.utils.decorators import method_decorator +from django.views.debug import DEBUG_ENGINE from django.views.decorators.csrf import requires_csrf_token +from django.views.generic.base import ContextMixin, View ERROR_404_TEMPLATE_NAME = "404.html" ERROR_403_TEMPLATE_NAME = "403.html" @@ -148,3 +152,63 @@ def permission_denied(request, exception, template_name=ERROR_403_TEMPLATE_NAME) return HttpResponseForbidden( template.render(request=request, context={"exception": str(exception)}) ) + + +@method_decorator(requires_csrf_token, name="dispatch") +class DefaultErrorView(ContextMixin, View): + status_code = None + context_by_status = { + 400: {"title": "Bad Request (400)", "details": ""}, + 403: {"title": "403 Forbidden", "details": ""}, + 404: { + "title": "Not Found", + "details": "The requested resource was not found on this server.", + }, + 500: {"title": "Server Error (500)", "details": ""}, + } + + def setup(self, request, exception=None, **kwargs): + self.exception = exception + return super().setup(request, **kwargs) + + def get(self, request, *args, **kwargs): + response_class = HttpResponse.response_class_by_status_code(self.status_code) + context = self.get_context_data(**kwargs) + try: + template = loader.get_template(self.get_template_name()) + content = template.render(context, request) + except TemplateDoesNotExist: + template = DEBUG_ENGINE.from_string(ERROR_PAGE_TEMPLATE % context) + content = template.render(context=Context(context)) + return response_class(content, status=self.status_code) + + def post(self, *args, **kwargs): + return self.get(*args, **kwargs) + + def get_template_name(self): + return f"{self.status_code}.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context |= self.context_by_status.get( + self.status_code, {"title": f"Error ({self.status_code})", "details": ""} + ) + context |= { + "request_path": quote(self.request.path), + "exception": self.exception_as_string(), + } + return context + + def exception_as_string(self): + if self.status_code == 404: + # Try to get an "interesting" exception message, if any (and not the + # ugly Resolver404 dictionary) + try: + message = self.exception.args[0] + except (AttributeError, IndexError): + pass + else: + if isinstance(message, str): + return message + return self.exception.__class__.__name__ + return str(self.exception) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 85ad0d400f..419b161916 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -1179,9 +1179,8 @@ details on these changes. ``django.contrib.gis.utils`` will be removed. * ``django.conf.urls.defaults`` will be removed. The functions - ``include()``, ``patterns()``, and ``url()``, plus - :data:`~django.conf.urls.handler404` and :data:`~django.conf.urls.handler500` - are now available through ``django.conf.urls``. + ``include()``, ``patterns()``, and ``url()``, plus ``handler404` and + ``handler500`` are now available through ``django.conf.urls``. * The functions ``setup_environ()`` and ``execute_manager()`` will be removed from :mod:`django.core.management`. This also means that the old (pre-1.4) diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 95eb03f35a..e460d70c6d 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -187,6 +187,11 @@ By default, this is :func:`django.views.defaults.bad_request`. If you implement a custom view, be sure it accepts ``request`` and ``exception`` arguments and returns an :class:`~django.http.HttpResponseBadRequest`. +.. deprecated:: 5.2 + + The ``handler`` handlers are deprecated and should be replaced by a + generic ``error_handler`` view function. + ``handler403`` ============== @@ -200,6 +205,12 @@ By default, this is :func:`django.views.defaults.permission_denied`. If you implement a custom view, be sure it accepts ``request`` and ``exception`` arguments and returns an :class:`~django.http.HttpResponseForbidden`. + +.. deprecated:: 5.2 + + The ``handler`` handlers are deprecated and should be replaced by a + generic ``error_handler`` view function. + ``handler404`` ============== @@ -212,6 +223,12 @@ By default, this is :func:`django.views.defaults.page_not_found`. If you implement a custom view, be sure it accepts ``request`` and ``exception`` arguments and returns an :class:`~django.http.HttpResponseNotFound`. + +.. deprecated:: 5.2 + + The ``handler`` handlers are deprecated and should be replaced by a + generic ``error_handler`` view function. + ``handler500`` ============== @@ -224,3 +241,8 @@ have runtime errors in view code. By default, this is :func:`django.views.defaults.server_error`. If you implement a custom view, be sure it accepts a ``request`` argument and returns an :class:`~django.http.HttpResponseServerError`. + +.. deprecated:: 5.2 + + The ``handler`` handlers are deprecated and should be replaced by a + generic ``error_handler`` view function. diff --git a/docs/ref/views.txt b/docs/ref/views.txt index b60ffc2ed8..169538e38d 100644 --- a/docs/ref/views.txt +++ b/docs/ref/views.txt @@ -58,8 +58,61 @@ parameter will be transparently passed to the view. Error views =========== -Django comes with a few views by default for handling HTTP errors. To override -these with your own custom views, see :ref:`customizing-error-views`. +When any uncaught exception is produced by a view of your site, Django is +calling an error handling view, which is by default the following view. + +.. _default_error_view: + +The default error view +---------------------- + +.. class:: defaults.DefaultErrorView + + .. versionadded:: 5.2 + + The view is a class-based-view inheriting from + :class:`~django.views.generic.base.ContextMixin` + and class:`~django.views.generic.base.View`, so refer to theses classes for + common attributes and methods. + + .. attribute:: status_code + + The status code of the HTTP response to return. + + .. attribute:: exception + + The exception that produced the error leading to the call of this error + view. + + .. method:: get_template_name() + + Return by default a name on the pattern ``.html``. + + .. method:: exception_as_string() + + Produce a string from the exception having triggered the error view. + +If you provide any template in your root template directory named after an error +response code (``404.html``, ``500.html``, etc.), that template will be used to +produce an error path. Otherwise, Django will produce a very simple page +containing an error title and a standard message. + +The context used to render the error templates contains the following keys: + + * ``title``: A short title for the error (like ``Not Found`` for a 404 + error). + * ``details``: A short message explaining the error, possibley empty. + * ``request_path``: The original request path of the view producing the + error, quoted to prevent a content injection attack. + * ``exception``: The uncaught exception that triggered the error view, as a + string. + +which either produces a "Not +Found" message or loads and renders the template ``404.html`` if you created it +in your root template directory. + +To override this default view with your custom view, see +:ref:`customizing-error-views`. .. _http_not_found_view: @@ -68,6 +121,11 @@ The 404 (page not found) view .. function:: defaults.page_not_found(request, exception, template_name='404.html') +.. deprecated:: 5.2 + + This view is deprecated as it has been replaced by the :ref:`default error + view <_default_error_view>`. + When you raise :exc:`~django.http.Http404` from within a view, Django loads a special view devoted to handling 404 errors. By default, it's the view :func:`django.views.defaults.page_not_found`, which either produces a "Not @@ -99,6 +157,11 @@ The 500 (server error) view .. function:: defaults.server_error(request, template_name='500.html') +.. deprecated:: 5.2 + + This view is deprecated as it has been replaced by the :ref:`default error + view <_default_error_view>`. + Similarly, Django executes special-case behavior in the case of runtime errors in view code. If a view results in an exception, Django will, by default, call the view ``django.views.defaults.server_error``, which either produces a @@ -119,6 +182,11 @@ The 403 (HTTP Forbidden) view .. function:: defaults.permission_denied(request, exception, template_name='403.html') +.. deprecated:: 5.2 + + This view is deprecated as it has been replaced by the :ref:`default error + view <_default_error_view>`. + In the same vein as the 404 and 500 views, Django has a view to handle 403 Forbidden errors. If a view results in a 403 exception then Django will, by default, call the view ``django.views.defaults.permission_denied``. @@ -148,6 +216,11 @@ The 400 (bad request) view .. function:: defaults.bad_request(request, exception, template_name='400.html') +.. deprecated:: 5.2 + + This view is deprecated as it has been replaced by the :ref:`default error + view <_default_error_view>`. + When a :exc:`~django.core.exceptions.SuspiciousOperation` is raised in Django, it may be handled by a component of Django (for example resetting the session data). If not specifically handled, Django will consider the current request a diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 8e57732725..f2a4e467fb 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -342,25 +342,28 @@ Error handling When Django can't find a match for the requested URL, or when an exception is raised, Django invokes an error-handling view. -The views to use for these cases are specified by four variables. Their -default values should suffice for most projects, but further customization is -possible by overriding their default values. +The default view Django uses for these cases is +:class:`django.views.defaults.DefaultErrorView`. This view should suffice for +most projects but further customization is possible by defining a custom view +to handle errors, either by subclassing this view or by creating your own. -See the documentation on :ref:`customizing error views +See the documentation on :ref:`customizing error view ` for the full details. -Such values can be set in your root URLconf. Setting these variables in any -other URLconf will have no effect. +.. versionchanged:: 5.2 -Values must be callables, or strings representing the full Python import path -to the view that should be called to handle the error condition at hand. + In previous versions, the views to use for these cases are specified by four + variables. -The variables are: + Values must be callables, or strings representing the full Python import path + to the view that should be called to handle the error condition at hand. -* ``handler400`` -- See :data:`django.conf.urls.handler400`. -* ``handler403`` -- See :data:`django.conf.urls.handler403`. -* ``handler404`` -- See :data:`django.conf.urls.handler404`. -* ``handler500`` -- See :data:`django.conf.urls.handler500`. + The variables are: + + * ``handler400`` + * ``handler403`` + * ``handler404`` + * ``handler500`` .. _including-other-urlconfs: diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index feb4eaa4ec..70e075372e 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -138,33 +138,46 @@ template. .. _customizing-error-views: -Customizing error views -======================= +Customizing error view +====================== -The default error views in Django should suffice for most web applications, -but can easily be overridden if you need any custom behavior. Specify the -handlers as seen below in your URLconf (setting them anywhere else will have no -effect). +The default error view in Django should suffice for most web applications, but +but can easily be overridden if you need any custom behavior. -The :func:`~django.views.defaults.page_not_found` view is overridden by -:data:`~django.conf.urls.handler404`:: +The default error view can be overridden by setting a view in the +``error_handler`` variable in your URLconf (setting it anywhere else will have +no effect):: - handler404 = "mysite.views.my_custom_page_not_found_view" + error_handler = views.my_custom_error_view -The :func:`~django.views.defaults.server_error` view is overridden by -:data:`~django.conf.urls.handler500`:: +or if you have a class-based-view:: - handler500 = "mysite.views.my_custom_error_view" + error_handler = views.MyCustomErrorView.as_view() -The :func:`~django.views.defaults.permission_denied` view is overridden by -:data:`~django.conf.urls.handler403`:: +.. versionchanged:: 5.2 - handler403 = "mysite.views.my_custom_permission_denied_view" + On previous versions, there are multiple variables to define to override + particular views. -The :func:`~django.views.defaults.bad_request` view is overridden by -:data:`~django.conf.urls.handler400`:: + The :func:`~django.views.defaults.page_not_found` view is overridden by + :data:`~django.conf.urls.handler404`:: - handler400 = "mysite.views.my_custom_bad_request_view" + handler404 = "mysite.views.my_custom_page_not_found_view" + + The :func:`~django.views.defaults.server_error` view is overridden by + :data:`~django.conf.urls.handler500`:: + + handler500 = "mysite.views.my_custom_error_view" + + The :func:`~django.views.defaults.permission_denied` view is overridden by + :data:`~django.conf.urls.handler403`:: + + handler403 = "mysite.views.my_custom_permission_denied_view" + + The :func:`~django.views.defaults.bad_request` view is overridden by + :data:`~django.conf.urls.handler400`:: + + handler400 = "mysite.views.my_custom_bad_request_view" .. seealso:: diff --git a/tests/check_framework/test_urls.py b/tests/check_framework/test_urls.py index a31c5fd856..c82de53df8 100644 --- a/tests/check_framework/test_urls.py +++ b/tests/check_framework/test_urls.py @@ -8,8 +8,9 @@ from django.core.checks.urls import ( check_url_settings, get_warning_for_invalid_pattern, ) -from django.test import SimpleTestCase +from django.test import SimpleTestCase, ignore_warnings from django.test.utils import override_settings +from django.utils.deprecation import RemovedInDjango61Warning class CheckUrlConfigTests(SimpleTestCase): @@ -243,10 +244,11 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): @override_settings( ROOT_URLCONF="check_framework.urls.bad_function_based_error_handlers", ) + @ignore_warnings(category=RemovedInDjango61Warning) def test_bad_function_based_handlers(self): result = check_custom_error_handlers(None) self.assertEqual(len(result), 4) - for code, num_params, error in zip([400, 403, 404, 500], [2, 2, 2, 1], result): + for code, error in zip([400, 403, 404, 500], result): with self.subTest("handler{}".format(code)): self.assertEqual( error, @@ -254,9 +256,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): "The custom handler{} view 'check_framework.urls." "bad_function_based_error_handlers.bad_handler' " "does not take the correct number of arguments " - "(request{}).".format( - code, ", exception" if num_params == 2 else "" - ), + "(request, exception).".format(code), id="urls.E007", ), ) @@ -264,10 +264,11 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): @override_settings( ROOT_URLCONF="check_framework.urls.bad_class_based_error_handlers", ) + @ignore_warnings(category=RemovedInDjango61Warning) def test_bad_class_based_handlers(self): result = check_custom_error_handlers(None) self.assertEqual(len(result), 4) - for code, num_params, error in zip([400, 403, 404, 500], [2, 2, 2, 1], result): + for code, error in zip([400, 403, 404, 500], result): with self.subTest("handler%s" % code): self.assertEqual( error, @@ -275,11 +276,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): "The custom handler%s view 'check_framework.urls." "bad_class_based_error_handlers.HandlerView.as_view." ".view' does not take the correct number of " - "arguments (request%s)." - % ( - code, - ", exception" if num_params == 2 else "", - ), + "arguments (request, exception)." % code, id="urls.E007", ), ) @@ -287,6 +284,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): @override_settings( ROOT_URLCONF="check_framework.urls.bad_error_handlers_invalid_path" ) + @ignore_warnings(category=RemovedInDjango61Warning) def test_bad_handlers_invalid_path(self): result = check_custom_error_handlers(None) paths = [ @@ -318,6 +316,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): @override_settings( ROOT_URLCONF="check_framework.urls.good_function_based_error_handlers", ) + @ignore_warnings(category=RemovedInDjango61Warning) def test_good_function_based_handlers(self): result = check_custom_error_handlers(None) self.assertEqual(result, []) @@ -325,6 +324,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase): @override_settings( ROOT_URLCONF="check_framework.urls.good_class_based_error_handlers", ) + @ignore_warnings(category=RemovedInDjango61Warning) def test_good_class_based_handlers(self): result = check_custom_error_handlers(None) self.assertEqual(result, []) diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 956cff11d9..96a61f0181 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -22,7 +22,8 @@ from django.middleware.csrf import ( get_token, rotate_token, ) -from django.test import SimpleTestCase, override_settings +from django.test import SimpleTestCase, ignore_warnings, override_settings +from django.utils.deprecation import RemovedInDjango61Warning from django.views.decorators.csrf import csrf_exempt, requires_csrf_token from .views import ( @@ -1477,6 +1478,7 @@ class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTest @override_settings(ROOT_URLCONF="csrf_tests.csrf_token_error_handler_urls", DEBUG=False) class CsrfInErrorHandlingViewsTests(CsrfFunctionTestMixin, SimpleTestCase): + @ignore_warnings(category=RemovedInDjango61Warning) def test_csrf_token_on_404_stays_constant(self): response = self.client.get("/does not exist/") # The error handler returns status code 599. diff --git a/tests/handlers/tests_custom_error_handlers.py b/tests/handlers/tests_custom_error_handlers.py index dc0b3c9ea6..12ce63f528 100644 --- a/tests/handlers/tests_custom_error_handlers.py +++ b/tests/handlers/tests_custom_error_handlers.py @@ -1,7 +1,13 @@ from django.core.exceptions import PermissionDenied from django.template.response import TemplateResponse -from django.test import SimpleTestCase, modify_settings, override_settings +from django.test import ( + SimpleTestCase, + ignore_warnings, + modify_settings, + override_settings, +) from django.urls import path +from django.utils.deprecation import RemovedInDjango61Warning class MiddlewareAccessingContent: @@ -38,6 +44,7 @@ handler403 = template_response_error_handler } ) class CustomErrorHandlerTests(SimpleTestCase): + @ignore_warnings(category=RemovedInDjango61Warning) def test_handler_renders_template_response(self): """ BaseHandler should render TemplateResponse if necessary. diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 91d3f237ec..ef09f3d64b 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -13,7 +13,13 @@ from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.http import HttpRequest, HttpResponsePermanentRedirect, HttpResponseRedirect from django.shortcuts import redirect -from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings +from django.test import ( + RequestFactory, + SimpleTestCase, + TestCase, + ignore_warnings, + override_settings, +) from django.test.utils import override_script_prefix from django.urls import ( NoReverseMatch, @@ -32,6 +38,7 @@ from django.urls import ( reverse_lazy, ) from django.urls.resolvers import RegexPattern +from django.utils.deprecation import RemovedInDjango61Warning from . import middleware, urlconf_outer, views from .utils import URLObject @@ -1473,12 +1480,15 @@ class ErrorHandlerResolutionTests(SimpleTestCase): self.resolver = URLResolver(RegexPattern(r"^$"), urlconf) self.callable_resolver = URLResolver(RegexPattern(r"^$"), urlconf_callables) + @ignore_warnings(category=RemovedInDjango61Warning) def test_named_handlers(self): for code in [400, 403, 404, 500]: with self.subTest(code=code): self.assertEqual(self.resolver.resolve_error_handler(code), empty_view) + @ignore_warnings(category=RemovedInDjango61Warning) def test_callable_handlers(self): + # After Django 6.1 removal, only test with 'error_handler' and one code. for code in [400, 403, 404, 500]: with self.subTest(code=code): self.assertEqual( diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py index 614fc460fc..eb1190c4f0 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py +++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -4,7 +4,9 @@ from .views import empty_view urlpatterns = [] +# RemovedInDjango61Warning: the handler<...> variables can be removed handler400 = empty_view handler403 = empty_view handler404 = empty_view handler500 = empty_view +error_handler = empty_view diff --git a/tests/urlpatterns_reverse/urls_without_handlers.py b/tests/urlpatterns_reverse/urls_without_handlers.py index 8d5ee0d347..d309d92906 100644 --- a/tests/urlpatterns_reverse/urls_without_handlers.py +++ b/tests/urlpatterns_reverse/urls_without_handlers.py @@ -1,4 +1,4 @@ -# A URLconf that doesn't define any handlerXXX. +# A URLconf that doesn't define any error handler view. from django.urls import path from .views import bad_view, empty_view