From 13e4abf83e5129b44c771b2204809792087abda4 Mon Sep 17 00:00:00 2001
From: Pavel Lysak
Date: Sat, 7 Sep 2019 20:08:12 +0300
Subject: [PATCH] Fixed #30752 -- Allowed using ExceptionReporter subclasses in
error reports.
---
django/conf/global_settings.py | 4 ++
django/utils/log.py | 2 +-
django/views/debug.py | 7 +++-
docs/howto/error-reporting.txt | 56 ++++++++++++++++++++++++++++
docs/ref/settings.txt | 14 +++++++
docs/releases/3.1.txt | 4 ++
tests/view_tests/tests/test_debug.py | 9 +++++
tests/view_tests/urls.py | 1 +
tests/view_tests/views.py | 18 ++++++++-
9 files changed, 112 insertions(+), 3 deletions(-)
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')