Fixed #24733 -- Passed the triggering exception to 40x error handlers
Thanks Tim Graham for the review.
This commit is contained in:
parent
bd53db5eab
commit
70779d9c1c
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -13,6 +14,7 @@ from django.core.exceptions import (
|
||||||
from django.db import connections, transaction
|
from django.db import connections, transaction
|
||||||
from django.http.multipartparser import MultiPartParserError
|
from django.http.multipartparser import MultiPartParserError
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
from django.utils.deprecation import RemovedInDjango21Warning
|
||||||
from django.utils.encoding import force_text
|
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 import debug
|
from django.views import debug
|
||||||
|
@ -80,10 +82,21 @@ class BaseHandler(object):
|
||||||
view = transaction.atomic(using=db.alias)(view)
|
view = transaction.atomic(using=db.alias)(view)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def get_exception_response(self, request, resolver, status_code):
|
def get_exception_response(self, request, resolver, status_code, exception):
|
||||||
try:
|
try:
|
||||||
callback, param_dict = resolver.resolve_error_handler(status_code)
|
callback, param_dict = resolver.resolve_error_handler(status_code)
|
||||||
response = callback(request, **param_dict)
|
# Unfortunately, inspect.getargspec result is not trustable enough
|
||||||
|
# depending on the callback wrapping in decorators (frequent for handlers).
|
||||||
|
# Falling back on try/except:
|
||||||
|
try:
|
||||||
|
response = callback(request, **dict(param_dict, exception=exception))
|
||||||
|
except TypeError:
|
||||||
|
warnings.warn(
|
||||||
|
"Error handlers should accept an exception parameter. Update "
|
||||||
|
"your code as this parameter will be required in Django 2.1",
|
||||||
|
RemovedInDjango21Warning, stacklevel=2
|
||||||
|
)
|
||||||
|
response = callback(request, **param_dict)
|
||||||
except:
|
except:
|
||||||
signals.got_request_exception.send(sender=self.__class__, request=request)
|
signals.got_request_exception.send(sender=self.__class__, request=request)
|
||||||
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
|
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
|
||||||
|
@ -171,25 +184,25 @@ class BaseHandler(object):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
response = debug.technical_404_response(request, exc)
|
response = debug.technical_404_response(request, exc)
|
||||||
else:
|
else:
|
||||||
response = self.get_exception_response(request, resolver, 404)
|
response = self.get_exception_response(request, resolver, 404, exc)
|
||||||
|
|
||||||
except PermissionDenied:
|
except PermissionDenied as exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Forbidden (Permission denied): %s', request.path,
|
'Forbidden (Permission denied): %s', request.path,
|
||||||
extra={
|
extra={
|
||||||
'status_code': 403,
|
'status_code': 403,
|
||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
response = self.get_exception_response(request, resolver, 403)
|
response = self.get_exception_response(request, resolver, 403, exc)
|
||||||
|
|
||||||
except MultiPartParserError:
|
except MultiPartParserError as exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Bad request (Unable to parse request body): %s', request.path,
|
'Bad request (Unable to parse request body): %s', request.path,
|
||||||
extra={
|
extra={
|
||||||
'status_code': 400,
|
'status_code': 400,
|
||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
response = self.get_exception_response(request, resolver, 400)
|
response = self.get_exception_response(request, resolver, 400, exc)
|
||||||
|
|
||||||
except SuspiciousOperation as exc:
|
except SuspiciousOperation as exc:
|
||||||
# The request logger receives events for any problematic request
|
# The request logger receives events for any problematic request
|
||||||
|
@ -205,7 +218,7 @@ class BaseHandler(object):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
return debug.technical_500_response(request, *sys.exc_info(), status_code=400)
|
return debug.technical_500_response(request, *sys.exc_info(), status_code=400)
|
||||||
|
|
||||||
response = self.get_exception_response(request, resolver, 400)
|
response = self.get_exception_response(request, resolver, 400, exc)
|
||||||
|
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
|
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django import http
|
from django import http
|
||||||
from django.template import Context, Engine, TemplateDoesNotExist, loader
|
from django.template import Context, Engine, TemplateDoesNotExist, loader
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from django.views.decorators.csrf import requires_csrf_token
|
from django.views.decorators.csrf import requires_csrf_token
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +9,7 @@ from django.views.decorators.csrf import requires_csrf_token
|
||||||
# therefore need @requires_csrf_token in case the template needs
|
# therefore need @requires_csrf_token in case the template needs
|
||||||
# {% csrf_token %}.
|
# {% csrf_token %}.
|
||||||
@requires_csrf_token
|
@requires_csrf_token
|
||||||
def page_not_found(request, template_name='404.html'):
|
def page_not_found(request, exception, template_name='404.html'):
|
||||||
"""
|
"""
|
||||||
Default 404 handler.
|
Default 404 handler.
|
||||||
|
|
||||||
|
@ -15,8 +17,24 @@ def page_not_found(request, template_name='404.html'):
|
||||||
Context:
|
Context:
|
||||||
request_path
|
request_path
|
||||||
The path of the requested URL (e.g., '/app/pages/bad_page/')
|
The path of the requested URL (e.g., '/app/pages/bad_page/')
|
||||||
|
exception
|
||||||
|
The message from the exception which triggered the 404 (if one was
|
||||||
|
supplied), or the exception class name
|
||||||
"""
|
"""
|
||||||
context = {'request_path': request.path}
|
exception_repr = exception.__class__.__name__
|
||||||
|
# Try to get an "interesting" exception message, if any (and not the ugly
|
||||||
|
# Resolver404 dictionary)
|
||||||
|
try:
|
||||||
|
message = exception.args[0]
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(message, six.text_type):
|
||||||
|
exception_repr = message
|
||||||
|
context = {
|
||||||
|
'request_path': request.path,
|
||||||
|
'exception': exception_repr,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
template = loader.get_template(template_name)
|
template = loader.get_template(template_name)
|
||||||
body = template.render(context, request)
|
body = template.render(context, request)
|
||||||
|
@ -46,7 +64,7 @@ def server_error(request, template_name='500.html'):
|
||||||
|
|
||||||
|
|
||||||
@requires_csrf_token
|
@requires_csrf_token
|
||||||
def bad_request(request, template_name='400.html'):
|
def bad_request(request, exception, template_name='400.html'):
|
||||||
"""
|
"""
|
||||||
400 error handler.
|
400 error handler.
|
||||||
|
|
||||||
|
@ -57,6 +75,7 @@ def bad_request(request, template_name='400.html'):
|
||||||
template = loader.get_template(template_name)
|
template = loader.get_template(template_name)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>', content_type='text/html')
|
return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>', content_type='text/html')
|
||||||
|
# No exception content is passed to the template, to not disclose any sensitive information.
|
||||||
return http.HttpResponseBadRequest(template.render())
|
return http.HttpResponseBadRequest(template.render())
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,7 +83,7 @@ def bad_request(request, template_name='400.html'):
|
||||||
# therefore need @requires_csrf_token in case the template needs
|
# therefore need @requires_csrf_token in case the template needs
|
||||||
# {% csrf_token %}.
|
# {% csrf_token %}.
|
||||||
@requires_csrf_token
|
@requires_csrf_token
|
||||||
def permission_denied(request, template_name='403.html'):
|
def permission_denied(request, exception, template_name='403.html'):
|
||||||
"""
|
"""
|
||||||
Permission denied (403) handler.
|
Permission denied (403) handler.
|
||||||
|
|
||||||
|
@ -78,4 +97,6 @@ def permission_denied(request, template_name='403.html'):
|
||||||
template = loader.get_template(template_name)
|
template = loader.get_template(template_name)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
return http.HttpResponseForbidden('<h1>403 Forbidden</h1>', content_type='text/html')
|
return http.HttpResponseForbidden('<h1>403 Forbidden</h1>', content_type='text/html')
|
||||||
return http.HttpResponseForbidden(template.render(request=request))
|
return http.HttpResponseForbidden(
|
||||||
|
template.render(request=request, context={'exception': force_text(exception)})
|
||||||
|
)
|
||||||
|
|
|
@ -58,6 +58,9 @@ details on these changes.
|
||||||
* The ``django.template.loaders.base.Loader.__call__()`` method will be
|
* The ``django.template.loaders.base.Loader.__call__()`` method will be
|
||||||
removed.
|
removed.
|
||||||
|
|
||||||
|
* Support for custom error views with a single positional parameter will be
|
||||||
|
dropped.
|
||||||
|
|
||||||
.. _deprecation-removed-in-2.0:
|
.. _deprecation-removed-in-2.0:
|
||||||
|
|
||||||
2.0
|
2.0
|
||||||
|
|
|
@ -61,7 +61,7 @@ these with your own custom views, see :ref:`customizing-error-views`.
|
||||||
The 404 (page not found) view
|
The 404 (page not found) view
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
.. function:: defaults.page_not_found(request, template_name='404.html')
|
.. function:: defaults.page_not_found(request, exception, template_name='404.html')
|
||||||
|
|
||||||
When you raise :exc:`~django.http.Http404` from within a view, Django loads a
|
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
|
special view devoted to handling 404 errors. By default, it's the view
|
||||||
|
@ -69,8 +69,10 @@ special view devoted to handling 404 errors. By default, it's the view
|
||||||
simple "Not Found" message or loads and renders the template ``404.html`` if
|
simple "Not Found" message or loads and renders the template ``404.html`` if
|
||||||
you created it in your root template directory.
|
you created it in your root template directory.
|
||||||
|
|
||||||
The default 404 view will pass one variable to the template: ``request_path``,
|
The default 404 view will pass two variables to the template: ``request_path``,
|
||||||
which is the URL that resulted in the error.
|
which is the URL that resulted in the error, and ``exception``, which is a
|
||||||
|
useful representation of the exception that triggered the view (e.g. containing
|
||||||
|
any message passed to a specific ``Http404`` instance).
|
||||||
|
|
||||||
Three things to note about 404 views:
|
Three things to note about 404 views:
|
||||||
|
|
||||||
|
@ -85,6 +87,12 @@ Three things to note about 404 views:
|
||||||
your 404 view will never be used, and your URLconf will be displayed
|
your 404 view will never be used, and your URLconf will be displayed
|
||||||
instead, with some debug information.
|
instead, with some debug information.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
The signature of ``page_not_found()`` changed. The function now accepts a
|
||||||
|
second parameter, the exception that triggered the error. A useful
|
||||||
|
representation of the exception is also passed in the template context.
|
||||||
|
|
||||||
.. _http_internal_server_error_view:
|
.. _http_internal_server_error_view:
|
||||||
|
|
||||||
The 500 (server error) view
|
The 500 (server error) view
|
||||||
|
@ -110,7 +118,7 @@ instead, with some debug information.
|
||||||
The 403 (HTTP Forbidden) view
|
The 403 (HTTP Forbidden) view
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
.. function:: defaults.permission_denied(request, template_name='403.html')
|
.. function:: defaults.permission_denied(request, exception, template_name='403.html')
|
||||||
|
|
||||||
In the same vein as the 404 and 500 views, Django has a view to handle 403
|
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
|
Forbidden errors. If a view results in a 403 exception then Django will, by
|
||||||
|
@ -118,7 +126,9 @@ default, call the view ``django.views.defaults.permission_denied``.
|
||||||
|
|
||||||
This view loads and renders the template ``403.html`` in your root template
|
This view loads and renders the template ``403.html`` in your root template
|
||||||
directory, or if this file does not exist, instead serves the text
|
directory, or if this file does not exist, instead serves the text
|
||||||
"403 Forbidden", as per :rfc:`2616` (the HTTP 1.1 Specification).
|
"403 Forbidden", as per :rfc:`2616` (the HTTP 1.1 Specification). The template
|
||||||
|
context contains ``exception``, which is the unicode representation of the
|
||||||
|
exception that triggered the view.
|
||||||
|
|
||||||
``django.views.defaults.permission_denied`` is triggered by a
|
``django.views.defaults.permission_denied`` is triggered by a
|
||||||
:exc:`~django.core.exceptions.PermissionDenied` exception. To deny access in a
|
:exc:`~django.core.exceptions.PermissionDenied` exception. To deny access in a
|
||||||
|
@ -131,12 +141,19 @@ view you can use code like this::
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
The signature of ``permission_denied()`` changed in Django 1.9. The function
|
||||||
|
now accepts a second parameter, the exception that triggered the error. The
|
||||||
|
unicode representation of the exception is also passed in the template
|
||||||
|
context.
|
||||||
|
|
||||||
.. _http_bad_request_view:
|
.. _http_bad_request_view:
|
||||||
|
|
||||||
The 400 (bad request) view
|
The 400 (bad request) view
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
.. function:: defaults.bad_request(request, template_name='400.html')
|
.. function:: defaults.bad_request(request, exception, template_name='400.html')
|
||||||
|
|
||||||
When a :exc:`~django.core.exceptions.SuspiciousOperation` is raised in Django,
|
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
|
it may be handled by a component of Django (for example resetting the session
|
||||||
|
@ -145,6 +162,14 @@ data). If not specifically handled, Django will consider the current request a
|
||||||
|
|
||||||
``django.views.defaults.bad_request``, is otherwise very similar to the
|
``django.views.defaults.bad_request``, is otherwise very similar to the
|
||||||
``server_error`` view, but returns with the status code 400 indicating that
|
``server_error`` view, but returns with the status code 400 indicating that
|
||||||
the error condition was the result of a client operation.
|
the error condition was the result of a client operation. By default, nothing
|
||||||
|
related to the exception that triggered the view is passed to the template
|
||||||
|
context, as the exception message might contain sensitive information like
|
||||||
|
filesystem paths.
|
||||||
|
|
||||||
``bad_request`` views are also only used when :setting:`DEBUG` is ``False``.
|
``bad_request`` views are also only used when :setting:`DEBUG` is ``False``.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
The signature of ``bad_request()`` changed in Django 1.9. The function
|
||||||
|
now accepts a second parameter, the exception that triggered the error.
|
||||||
|
|
|
@ -269,6 +269,9 @@ Requests and Responses
|
||||||
|
|
||||||
* The debug view now shows details of chained exceptions on Python 3.
|
* The debug view now shows details of chained exceptions on Python 3.
|
||||||
|
|
||||||
|
* The default 40x error views now accept a second positional parameter, the
|
||||||
|
exception that triggered the view.
|
||||||
|
|
||||||
Tests
|
Tests
|
||||||
^^^^^
|
^^^^^
|
||||||
|
|
||||||
|
@ -520,6 +523,10 @@ Miscellaneous
|
||||||
Therefore, the ``@skipIfCustomUser`` decorator is no longer needed to
|
Therefore, the ``@skipIfCustomUser`` decorator is no longer needed to
|
||||||
decorate tests in ``django.contrib.auth``.
|
decorate tests in ``django.contrib.auth``.
|
||||||
|
|
||||||
|
* If you customized some :ref:`error handlers <error-views>`, the view
|
||||||
|
signatures with only one request parameter are deprecated. The views should
|
||||||
|
now also accept a second ``exception`` positional parameter.
|
||||||
|
|
||||||
.. removed-features-1.9:
|
.. removed-features-1.9:
|
||||||
|
|
||||||
Features removed in 1.9
|
Features removed in 1.9
|
||||||
|
|
|
@ -81,7 +81,7 @@ class DebugViewTests(LoggingCaptureMixin, TestCase):
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'loaders': [
|
'loaders': [
|
||||||
('django.template.loaders.locmem.Loader', {
|
('django.template.loaders.locmem.Loader', {
|
||||||
'403.html': 'This is a test template for a 403 error.',
|
'403.html': 'This is a test template for a 403 error ({{ exception }}).',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -89,6 +89,7 @@ class DebugViewTests(LoggingCaptureMixin, TestCase):
|
||||||
def test_403_template(self):
|
def test_403_template(self):
|
||||||
response = self.client.get('/raises403/')
|
response = self.client.get('/raises403/')
|
||||||
self.assertContains(response, 'test template', status_code=403)
|
self.assertContains(response, 'test template', status_code=403)
|
||||||
|
self.assertContains(response, '(Insufficient Permissions).', status_code=403)
|
||||||
|
|
||||||
def test_404(self):
|
def test_404(self):
|
||||||
response = self.client.get('/raises404/')
|
response = self.client.get('/raises404/')
|
||||||
|
|
|
@ -79,7 +79,8 @@ class DefaultsTests(TestCase):
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'loaders': [
|
'loaders': [
|
||||||
('django.template.loaders.locmem.Loader', {
|
('django.template.loaders.locmem.Loader', {
|
||||||
'404.html': 'This is a test template for a 404 error.',
|
'404.html': 'This is a test template for a 404 error '
|
||||||
|
'(path: {{ request_path }}, exception: {{ exception }}).',
|
||||||
'500.html': 'This is a test template for a 500 error.',
|
'500.html': 'This is a test template for a 500 error.',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -90,10 +91,13 @@ class DefaultsTests(TestCase):
|
||||||
Test that 404.html and 500.html templates are picked by their respective
|
Test that 404.html and 500.html templates are picked by their respective
|
||||||
handler.
|
handler.
|
||||||
"""
|
"""
|
||||||
for code, url in ((404, '/non_existing_url/'), (500, '/server_error/')):
|
response = self.client.get('/server_error/')
|
||||||
response = self.client.get(url)
|
self.assertContains(response, "test template for a 500 error", status_code=500)
|
||||||
self.assertContains(response, "test template for a %d error" % code,
|
response = self.client.get('/no_such_url/')
|
||||||
status_code=code)
|
self.assertContains(response, 'path: /no_such_url/', status_code=404)
|
||||||
|
self.assertContains(response, 'exception: Resolver404', status_code=404)
|
||||||
|
response = self.client.get('/technical404/')
|
||||||
|
self.assertContains(response, 'exception: Testing technical 404.', status_code=404)
|
||||||
|
|
||||||
def test_get_absolute_url_attributes(self):
|
def test_get_absolute_url_attributes(self):
|
||||||
"A model can set attributes on the get_absolute_url method"
|
"A model can set attributes on the get_absolute_url method"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from functools import partial
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
@ -57,7 +58,7 @@ urlpatterns = [
|
||||||
url(r'^$', views.index_page),
|
url(r'^$', views.index_page),
|
||||||
|
|
||||||
# Default views
|
# Default views
|
||||||
url(r'^non_existing_url/', defaults.page_not_found),
|
url(r'^non_existing_url/', partial(defaults.page_not_found, exception=None)),
|
||||||
url(r'^server_error/', defaults.server_error),
|
url(r'^server_error/', defaults.server_error),
|
||||||
|
|
||||||
# a view that raises an exception for the debug view
|
# a view that raises an exception for the debug view
|
||||||
|
|
|
@ -52,7 +52,7 @@ def raises400(request):
|
||||||
|
|
||||||
|
|
||||||
def raises403(request):
|
def raises403(request):
|
||||||
raise PermissionDenied
|
raise PermissionDenied("Insufficient Permissions")
|
||||||
|
|
||||||
|
|
||||||
def raises404(request):
|
def raises404(request):
|
||||||
|
|
Loading…
Reference in New Issue