Browse Source

Fixed #30752 -- Allowed using ExceptionReporter subclasses in error reports.

Pavel Lysak 5 years ago
parent
commit
13e4abf83e

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

+ 1 - 1
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:

+ 6 - 1
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

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

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

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

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

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

+ 17 - 1
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')