Fixed #25099 -- Cleaned up HttpRequest representations in error reporting.

This commit is contained in:
Vlastimil Zíma 2015-07-09 17:27:07 +02:00 committed by Tim Graham
parent 6bdd3840be
commit 8f8c54f70b
8 changed files with 34 additions and 103 deletions

View File

@ -1,7 +1,6 @@
from django.http.cookie import SimpleCookie, parse_cookie from django.http.cookie import SimpleCookie, parse_cookie
from django.http.request import ( from django.http.request import (
HttpRequest, QueryDict, RawPostDataException, UnreadablePostError, HttpRequest, QueryDict, RawPostDataException, UnreadablePostError,
build_request_repr,
) )
from django.http.response import ( from django.http.response import (
BadHeaderError, FileResponse, Http404, HttpResponse, BadHeaderError, FileResponse, Http404, HttpResponse,
@ -14,7 +13,7 @@ from django.http.utils import conditional_content_removal
__all__ = [ __all__ = [
'SimpleCookie', 'parse_cookie', 'HttpRequest', 'QueryDict', 'SimpleCookie', 'parse_cookie', 'HttpRequest', 'QueryDict',
'RawPostDataException', 'UnreadablePostError', 'build_request_repr', 'RawPostDataException', 'UnreadablePostError',
'HttpResponse', 'StreamingHttpResponse', 'HttpResponseRedirect', 'HttpResponse', 'StreamingHttpResponse', 'HttpResponseRedirect',
'HttpResponsePermanentRedirect', 'HttpResponseNotModified', 'HttpResponsePermanentRedirect', 'HttpResponseNotModified',
'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound', 'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound',

View File

@ -5,7 +5,6 @@ import re
import sys import sys
from io import BytesIO from io import BytesIO
from itertools import chain from itertools import chain
from pprint import pformat
from django.conf import settings from django.conf import settings
from django.core import signing from django.core import signing
@ -465,52 +464,6 @@ class QueryDict(MultiValueDict):
return '&'.join(output) return '&'.join(output)
def build_request_repr(request, path_override=None, GET_override=None,
POST_override=None, COOKIES_override=None,
META_override=None):
"""
Builds and returns the request's representation string. The request's
attributes may be overridden by pre-processed values.
"""
# Since this is called as part of error handling, we need to be very
# robust against potentially malformed input.
try:
get = (pformat(GET_override)
if GET_override is not None
else pformat(request.GET))
except Exception:
get = '<could not parse>'
if request._post_parse_error:
post = '<could not parse>'
else:
try:
post = (pformat(POST_override)
if POST_override is not None
else pformat(request.POST))
except Exception:
post = '<could not parse>'
try:
cookies = (pformat(COOKIES_override)
if COOKIES_override is not None
else pformat(request.COOKIES))
except Exception:
cookies = '<could not parse>'
try:
meta = (pformat(META_override)
if META_override is not None
else pformat(request.META))
except Exception:
meta = '<could not parse>'
path = path_override if path_override is not None else request.path
return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' %
(request.__class__.__name__,
path,
six.text_type(get),
six.text_type(post),
six.text_type(cookies),
six.text_type(meta)))
# It's neither necessary nor appropriate to use # It's neither necessary nor appropriate to use
# django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, # django.utils.encoding.smart_text for parsing URLs and form inputs. Thus,
# this slightly more restricted function, used by QueryDict. # this slightly more restricted function, used by QueryDict.

View File

@ -4,14 +4,14 @@ import logging
import logging.config # needed when logging_config doesn't start with logging.config import logging.config # needed when logging_config doesn't start with logging.config
import sys import sys
import warnings import warnings
from copy import copy
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from django.core.mail import get_connection from django.core.mail import get_connection
from django.utils.deprecation import RemovedInNextVersionWarning from django.utils.deprecation import RemovedInNextVersionWarning
from django.utils.encoding import force_text
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.views.debug import ExceptionReporter, get_exception_reporter_filter from django.views.debug import ExceptionReporter
# Default logging for Django. This sends an email to the site admins on every # Default logging for Django. This sends an email to the site admins on every
# HTTP 500 error. Depending on DEBUG, all other log records are either sent to # HTTP 500 error. Depending on DEBUG, all other log records are either sent to
@ -90,24 +90,27 @@ class AdminEmailHandler(logging.Handler):
else 'EXTERNAL'), else 'EXTERNAL'),
record.getMessage() record.getMessage()
) )
filter = get_exception_reporter_filter(request)
request_repr = '\n{}'.format(force_text(filter.get_request_repr(request)))
except Exception: except Exception:
subject = '%s: %s' % ( subject = '%s: %s' % (
record.levelname, record.levelname,
record.getMessage() record.getMessage()
) )
request = None request = None
request_repr = "unavailable"
subject = self.format_subject(subject) subject = self.format_subject(subject)
# Since we add a nicely formatted traceback on our own, create a copy
# of the log record without the exception data.
no_exc_record = copy(record)
no_exc_record.exc_info = None
no_exc_record.exc_text = None
if record.exc_info: if record.exc_info:
exc_info = record.exc_info exc_info = record.exc_info
else: else:
exc_info = (None, record.getMessage(), None) exc_info = (None, record.getMessage(), None)
message = "%s\n\nRequest repr(): %s" % (self.format(record), request_repr)
reporter = ExceptionReporter(request, is_email=True, *exc_info) reporter = ExceptionReporter(request, is_email=True, *exc_info)
message = "%s\n\n%s" % (self.format(no_exc_record), reporter.get_traceback_text())
html_message = reporter.get_traceback_html() if self.include_html else None html_message = reporter.get_traceback_html() if self.include_html else None
self.send_mail(subject, message, fail_silently=True, html_message=html_message) self.send_mail(subject, message, fail_silently=True, html_message=html_message)

View File

@ -6,9 +6,7 @@ import types
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import Resolver404, resolve from django.core.urlresolvers import Resolver404, resolve
from django.http import ( from django.http import HttpResponse, HttpResponseNotFound
HttpRequest, HttpResponse, HttpResponseNotFound, build_request_repr,
)
from django.template import Context, Engine, TemplateDoesNotExist from django.template import Context, Engine, TemplateDoesNotExist
from django.template.defaultfilters import force_escape, pprint from django.template.defaultfilters import force_escape, pprint
from django.utils import lru_cache, six, timezone from django.utils import lru_cache, six, timezone
@ -113,12 +111,6 @@ class ExceptionReporterFilter(object):
contain lenient default behaviors. contain lenient default behaviors.
""" """
def get_request_repr(self, request):
if request is None:
return repr(None)
else:
return build_request_repr(request, POST_override=self.get_post_parameters(request))
def get_post_parameters(self, request): def get_post_parameters(self, request):
if request is None: if request is None:
return {} return {}
@ -186,16 +178,13 @@ class SafeExceptionReporterFilter(ExceptionReporterFilter):
def cleanse_special_types(self, request, value): def cleanse_special_types(self, request, value):
try: try:
# If value is lazy or a complex object of another kind, this check # If value is lazy or a complex object of another kind, this check
# might raise an exception. isinstance checks that lazy HttpRequests # might raise an exception. isinstance checks that lazy
# or MultiValueDicts will have a return value. # MultiValueDicts will have a return value.
is_request = isinstance(value, HttpRequest) is_multivalue_dict = isinstance(value, MultiValueDict)
except Exception as e: except Exception as e:
return '{!r} while evaluating {!r}'.format(e, value) return '{!r} while evaluating {!r}'.format(e, value)
if is_request: if is_multivalue_dict:
# Cleanse the request's POST parameters.
value = self.get_request_repr(value)
elif isinstance(value, MultiValueDict):
# Cleanse MultiValueDicts (request.POST is the one we usually care about) # Cleanse MultiValueDicts (request.POST is the one we usually care about)
value = self.get_cleansed_multivaluedict(request, value) value = self.get_cleansed_multivaluedict(request, value)
return value return value
@ -1126,9 +1115,11 @@ Settings:
Using settings module {{ settings.SETTINGS_MODULE }}{% for k, v in settings.items|dictsort:"0" %} Using settings module {{ settings.SETTINGS_MODULE }}{% for k, v in settings.items|dictsort:"0" %}
{{ k }} = {{ v|stringformat:"r" }}{% endfor %} {{ k }} = {{ v|stringformat:"r" }}{% endfor %}
{% if not is_email %}
You're seeing this error because you have DEBUG = True in your You're seeing this error because you have DEBUG = True in your
Django settings file. Change that to False, and Django will Django settings file. Change that to False, and Django will
display a standard page generated by the handler for this status code. display a standard page generated by the handler for this status code.
{% endif %}
""") """)
TECHNICAL_404_TEMPLATE = """ TECHNICAL_404_TEMPLATE = """

View File

@ -255,13 +255,6 @@ following methods:
Returns ``True`` to activate the filtering operated in the other methods. Returns ``True`` to activate the filtering operated in the other methods.
By default the filter is active if :setting:`DEBUG` is ``False``. By default the filter is active if :setting:`DEBUG` is ``False``.
.. method:: SafeExceptionReporterFilter.get_request_repr(request)
Returns the representation string of the request object, that is, the
value that would be returned by ``repr(request)``, except it uses the
filtered dictionary of POST parameters as determined by
:meth:`SafeExceptionReporterFilter.get_post_parameters`.
.. method:: SafeExceptionReporterFilter.get_post_parameters(request) .. method:: SafeExceptionReporterFilter.get_post_parameters(request)
Returns the filtered dictionary of POST parameters. By default it replaces Returns the filtered dictionary of POST parameters. By default it replaces

View File

@ -683,6 +683,20 @@ console, for example.
If you are overriding Django's default logging, you should check to see how If you are overriding Django's default logging, you should check to see how
your configuration merges with the new defaults. your configuration merges with the new defaults.
``HttpRequest`` details in error reporting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It was redundant to display the full details of the
:class:`~django.http.HttpRequest` each time it appeared as a stack frame
variable in the HTML version of the debug page and error email. Thus, the HTTP
request will now display the same standard representation as other variables
(``repr(request)``). As a result, the method
``ExceptionReporterFilter.get_request_repr()`` was removed.
The contents of the text version of the email were modified to provide a
traceback of the same structure as in the case of AJAX requests. The traceback
details are rendered by the ``ExceptionReporter.get_traceback_text()`` method.
Removal of time zone aware global adapters and converters for datetimes Removal of time zone aware global adapters and converters for datetimes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -334,7 +334,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
msg = mail.outbox[0] msg = mail.outbox[0]
self.assertEqual(msg.to, ['admin@example.com']) self.assertEqual(msg.to, ['admin@example.com'])
self.assertEqual(msg.subject, "[Django] ERROR (EXTERNAL IP): message") self.assertEqual(msg.subject, "[Django] ERROR (EXTERNAL IP): message")
self.assertIn("path:%s" % url_path, msg.body) self.assertIn("Report at %s" % url_path, msg.body)
@override_settings( @override_settings(
MANAGERS=[('manager', 'manager@example.com')], MANAGERS=[('manager', 'manager@example.com')],
@ -419,7 +419,7 @@ class SecurityLoggerTest(SimpleTestCase):
def test_suspicious_email_admins(self): def test_suspicious_email_admins(self):
self.client.get('/suspicious/') self.client.get('/suspicious/')
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertIn('path:/suspicious/,', mail.outbox[0].body) self.assertIn('Report at /suspicious/', mail.outbox[0].body)
class SettingsCustomLoggingTest(AdminScriptTestCase): class SettingsCustomLoggingTest(AdminScriptTestCase):

View File

@ -10,7 +10,7 @@ from django.core.exceptions import SuspiciousOperation
from django.core.handlers.wsgi import LimitedStream, WSGIRequest from django.core.handlers.wsgi import LimitedStream, WSGIRequest
from django.http import ( from django.http import (
HttpRequest, HttpResponse, RawPostDataException, UnreadablePostError, HttpRequest, HttpResponse, RawPostDataException, UnreadablePostError,
build_request_repr, parse_cookie, parse_cookie,
) )
from django.test import RequestFactory, SimpleTestCase, override_settings from django.test import RequestFactory, SimpleTestCase, override_settings
from django.test.client import FakePayload from django.test.client import FakePayload
@ -60,9 +60,6 @@ class RequestsTests(SimpleTestCase):
request.COOKIES = {'post-key': 'post-value'} request.COOKIES = {'post-key': 'post-value'}
request.META = {'post-key': 'post-value'} request.META = {'post-key': 'post-value'}
self.assertEqual(repr(request), str_prefix("<HttpRequest: GET '/somepath/'>")) self.assertEqual(repr(request), str_prefix("<HttpRequest: GET '/somepath/'>"))
self.assertEqual(build_request_repr(request), str_prefix("<HttpRequest\npath:/somepath/,\nGET:{%(_)s'get-key': %(_)s'get-value'},\nPOST:{%(_)s'post-key': %(_)s'post-value'},\nCOOKIES:{%(_)s'post-key': %(_)s'post-value'},\nMETA:{%(_)s'post-key': %(_)s'post-value'}>"))
self.assertEqual(build_request_repr(request, path_override='/otherpath/', GET_override={'a': 'b'}, POST_override={'c': 'd'}, COOKIES_override={'e': 'f'}, META_override={'g': 'h'}),
str_prefix("<HttpRequest\npath:/otherpath/,\nGET:{%(_)s'a': %(_)s'b'},\nPOST:{%(_)s'c': %(_)s'd'},\nCOOKIES:{%(_)s'e': %(_)s'f'},\nMETA:{%(_)s'g': %(_)s'h'}>"))
def test_httprequest_repr_invalid_method_and_path(self): def test_httprequest_repr_invalid_method_and_path(self):
request = HttpRequest() request = HttpRequest()
@ -74,22 +71,6 @@ class RequestsTests(SimpleTestCase):
request.path = "" request.path = ""
self.assertEqual(repr(request), str_prefix("<HttpRequest>")) self.assertEqual(repr(request), str_prefix("<HttpRequest>"))
def test_bad_httprequest_repr(self):
"""
If an exception occurs when parsing GET, POST, COOKIES, or META, the
repr of the request should show it.
"""
class Bomb(object):
"""An object that raises an exception when printed out."""
def __repr__(self):
raise Exception('boom!')
bomb = Bomb()
for attr in ['GET', 'POST', 'COOKIES', 'META']:
request = HttpRequest()
setattr(request, attr, {'bomb': bomb})
self.assertIn('%s:<could not parse>' % attr, build_request_repr(request))
def test_wsgirequest(self): def test_wsgirequest(self):
request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'wsgi.input': BytesIO(b'')}) request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'wsgi.input': BytesIO(b'')})
self.assertEqual(list(request.GET.keys()), []) self.assertEqual(list(request.GET.keys()), [])
@ -147,9 +128,6 @@ class RequestsTests(SimpleTestCase):
request.COOKIES = {'post-key': 'post-value'} request.COOKIES = {'post-key': 'post-value'}
request.META = {'post-key': 'post-value'} request.META = {'post-key': 'post-value'}
self.assertEqual(repr(request), str_prefix("<WSGIRequest: GET '/somepath/'>")) self.assertEqual(repr(request), str_prefix("<WSGIRequest: GET '/somepath/'>"))
self.assertEqual(build_request_repr(request), str_prefix("<WSGIRequest\npath:/somepath/,\nGET:{%(_)s'get-key': %(_)s'get-value'},\nPOST:{%(_)s'post-key': %(_)s'post-value'},\nCOOKIES:{%(_)s'post-key': %(_)s'post-value'},\nMETA:{%(_)s'post-key': %(_)s'post-value'}>"))
self.assertEqual(build_request_repr(request, path_override='/otherpath/', GET_override={'a': 'b'}, POST_override={'c': 'd'}, COOKIES_override={'e': 'f'}, META_override={'g': 'h'}),
str_prefix("<WSGIRequest\npath:/otherpath/,\nGET:{%(_)s'a': %(_)s'b'},\nPOST:{%(_)s'c': %(_)s'd'},\nCOOKIES:{%(_)s'e': %(_)s'f'},\nMETA:{%(_)s'g': %(_)s'h'}>"))
def test_wsgirequest_path_info(self): def test_wsgirequest_path_info(self):
def wsgi_str(path_info): def wsgi_str(path_info):