diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 830ba25408..09c9b95d26 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -567,6 +567,10 @@ LOGGING_CONFIG = 'logging.config.dictConfig' # Custom logging configuration. LOGGING = {} +# Default exception reporter class used in case none has been +# specifically assigned to the HttpRequest instance. +DEFAULT_EXCEPTION_REPORTER = 'django.views.debug.ExceptionReporter' + # Default exception reporter filter class used in case none has been # specifically assigned to the HttpRequest instance. DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter' diff --git a/django/utils/log.py b/django/utils/log.py index e40d87159c..717c15814c 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -86,7 +86,7 @@ class AdminEmailHandler(logging.Handler): super().__init__() self.include_html = include_html self.email_backend = email_backend - self.reporter_class = import_string(reporter_class or 'django.views.debug.ExceptionReporter') + self.reporter_class = import_string(reporter_class or settings.DEFAULT_EXCEPTION_REPORTER) def emit(self, record): try: diff --git a/django/views/debug.py b/django/views/debug.py index 13dabf165b..1761d6904a 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -47,7 +47,7 @@ def technical_500_response(request, exc_type, exc_value, tb, status_code=500): Create a technical server error response. The last three arguments are the values returned from sys.exc_info() and friends. """ - reporter = ExceptionReporter(request, exc_type, exc_value, tb) + reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb) if request.is_ajax(): text = reporter.get_traceback_text() return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8') @@ -67,6 +67,11 @@ def get_exception_reporter_filter(request): return getattr(request, 'exception_reporter_filter', default_filter) +def get_exception_reporter_class(request): + default_exception_reporter_class = import_string(settings.DEFAULT_EXCEPTION_REPORTER) + return getattr(request, 'exception_reporter_class', default_exception_reporter_class) + + class SafeExceptionReporterFilter: """ Use annotations made by the sensitive_post_parameters and diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index e145897b2a..13043cf387 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -305,6 +305,62 @@ following attributes and methods: traceback frame. Sensitive values are replaced with :attr:`cleansed_substitute`. +.. versionadded:: 3.1 + +If you need to customize error reports beyond filtering you may specify a +custom error reporter class by defining the +:setting:`DEFAULT_EXCEPTION_REPORTER` setting:: + + DEFAULT_EXCEPTION_REPORTER = 'path.to.your.CustomExceptionReporter' + +The exception reporter is responsible for compiling the exception report data, +and formatting it as text or HTML appropriately. (The exception reporter uses +:setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` when preparing the exception +report data.) + +Your custom reporter class needs to inherit from +:class:`django.views.debug.ExceptionReporter`. + +.. class:: ExceptionReporter + + .. method:: get_traceback_data() + + Return a dictionary containing traceback information. + + This is the main extension point for customizing exception reports, for + example:: + + from django.views.debug import ExceptionReporter + + + class CustomExceptionReporter(ExceptionReporter): + def get_traceback_data(self): + data = super().get_traceback_data() + # ... remove/add something here ... + return data + + .. method:: get_traceback_html() + + Return HTML version of exception report. + + Used for HTML version of debug 500 HTTP error page. + + .. method:: get_traceback_text() + + Return plain text version of exception report. + + Used for plain text version of debug 500 HTTP error page and email + reports. + +As with the filter class, you may control which exception reporter class to use +within any given view by setting the ``HttpRequest``’s +``exception_reporter_class`` attribute:: + + def my_view(request): + if request.user.is_authenticated: + request.exception_reporter_class = CustomExceptionReporter() + ... + .. seealso:: You can also set up custom error reporting by writing a custom piece of diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 51bbba35f0..917ffeebb4 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1250,6 +1250,19 @@ Default: ``'utf-8'`` Default charset to use for all ``HttpResponse`` objects, if a MIME type isn't manually specified. Used when constructing the ``Content-Type`` header. +.. setting:: DEFAULT_EXCEPTION_REPORTER + +``DEFAULT_EXCEPTION_REPORTER`` +------------------------------ + +.. versionadded:: 3.1 + +Default: ``'``:class:`django.views.debug.ExceptionReporter`\ ``'`` + +Default exception reporter class to be used if none has been assigned to the +:class:`~django.http.HttpRequest` instance yet. See +:ref:`custom-error-reports`. + .. setting:: DEFAULT_EXCEPTION_REPORTER_FILTER ``DEFAULT_EXCEPTION_REPORTER_FILTER`` @@ -3537,6 +3550,7 @@ Email Error reporting --------------- +* :setting:`DEFAULT_EXCEPTION_REPORTER` * :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` * :setting:`IGNORABLE_404_URLS` * :setting:`MANAGERS` diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 064aadd34a..ced98c2e47 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -173,6 +173,10 @@ Error Reporting :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` when applying settings filtering. +* The new :setting:`DEFAULT_EXCEPTION_REPORTER` allows providing a + :class:`django.views.debug.ExceptionReporter` subclass to customize exception + report generation. See :ref:`custom-error-reports` for details. + File Storage ~~~~~~~~~~~~ diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 020af1a19c..07d8208e19 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -249,6 +249,15 @@ class DebugViewTests(SimpleTestCase): response = self.client.get('/path-post/1/') self.assertContains(response, 'Page not found', status_code=404) + def test_exception_reporter_from_request(self): + response = self.client.get('/custom_reporter_class_view/') + self.assertContains(response, 'custom traceback text', status_code=500) + + @override_settings(DEFAULT_EXCEPTION_REPORTER='view_tests.views.CustomExceptionReporter') + def test_exception_reporter_from_settings(self): + response = self.client.get('/raises500/') + self.assertContains(response, 'custom traceback text', status_code=500) + class DebugViewQueriesAllowedTests(SimpleTestCase): # May need a query to initialize MySQL connection diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 34415b06e0..6c6f73467a 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('raises403/', views.raises403), path('raises404/', views.raises404), path('raises500/', views.raises500), + path('custom_reporter_class_view/', views.custom_reporter_class_view), path('technical404/', views.technical404, name='my404'), path('classbased404/', views.Http404View.as_view()), diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index ce0079a355..36c7bda4b4 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -10,7 +10,7 @@ from django.template import TemplateDoesNotExist from django.urls import get_resolver from django.views import View from django.views.debug import ( - SafeExceptionReporterFilter, technical_500_response, + ExceptionReporter, SafeExceptionReporterFilter, technical_500_response, ) from django.views.decorators.debug import ( sensitive_post_parameters, sensitive_variables, @@ -227,6 +227,22 @@ def custom_exception_reporter_filter_view(request): return technical_500_response(request, *exc_info) +class CustomExceptionReporter(ExceptionReporter): + custom_traceback_text = 'custom traceback text' + + def get_traceback_html(self): + return self.custom_traceback_text + + +def custom_reporter_class_view(request): + request.exception_reporter_class = CustomExceptionReporter + try: + raise Exception + except Exception: + exc_info = sys.exc_info() + return technical_500_response(request, *exc_info) + + class Klass: @sensitive_variables('sauce')