diff options
| author | Carl Meyer <carl@oddbird.net> | 2016-06-14 00:41:58 -0700 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2016-06-17 15:55:55 -0400 |
| commit | 69de988f9237fe978c5fca363ca2d2622f8e2ccd (patch) | |
| tree | 85e2b35e3173b62f395d78a018a454d999097606 /django | |
| parent | 34fbec3cb48f41dda8daecf2e8330233621fa059 (diff) | |
[1.10.x] Refs #26601 -- Improved backwards-compatibility of DEP 5 middleware exception handling.
Backport of 7d1b69dbe7f72ac04d2513f0468fe2146231b286 from master
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/flatpages/middleware.py | 14 | ||||
| -rw-r--r-- | django/contrib/redirects/middleware.py | 9 | ||||
| -rw-r--r-- | django/core/handlers/base.py | 77 | ||||
| -rw-r--r-- | django/core/handlers/exception.py | 135 | ||||
| -rw-r--r-- | django/middleware/exception.py | 78 | ||||
| -rw-r--r-- | django/middleware/locale.py | 15 | ||||
| -rw-r--r-- | django/utils/deprecation.py | 8 |
7 files changed, 166 insertions, 170 deletions
diff --git a/django/contrib/flatpages/middleware.py b/django/contrib/flatpages/middleware.py index 4c6760535d..9c6a7273cc 100644 --- a/django/contrib/flatpages/middleware.py +++ b/django/contrib/flatpages/middleware.py @@ -1,20 +1,10 @@ from django.conf import settings from django.contrib.flatpages.views import flatpage from django.http import Http404 -from django.middleware.exception import ExceptionMiddleware +from django.utils.deprecation import MiddlewareMixin -class FlatpageFallbackMiddleware(ExceptionMiddleware): - - def __init__(self, get_response=None): - # This override makes get_response optional during the - # MIDDLEWARE_CLASSES deprecation. - super(FlatpageFallbackMiddleware, self).__init__(get_response) - - def __call__(self, request): - response = super(FlatpageFallbackMiddleware, self).__call__(request) - return self.process_response(request, response) - +class FlatpageFallbackMiddleware(MiddlewareMixin): def process_response(self, request, response): if response.status_code != 404: return response # No need to check for a flatpage for non-404 responses. diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index e3ee982a8b..f5c9f85588 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -6,11 +6,10 @@ from django.conf import settings from django.contrib.redirects.models import Redirect from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ImproperlyConfigured -from django.middleware.exception import ExceptionMiddleware +from django.utils.deprecation import MiddlewareMixin -class RedirectFallbackMiddleware(ExceptionMiddleware): - +class RedirectFallbackMiddleware(MiddlewareMixin): # Defined as class-level attributes to be subclassing-friendly. response_gone_class = http.HttpResponseGone response_redirect_class = http.HttpResponsePermanentRedirect @@ -23,10 +22,6 @@ class RedirectFallbackMiddleware(ExceptionMiddleware): ) super(RedirectFallbackMiddleware, self).__init__(get_response) - def __call__(self, request): - response = super(RedirectFallbackMiddleware, self).__call__(request) - return self.process_response(request, response) - def process_response(self, request, response): # No need to check for a redirect for non-404 responses. if response.status_code != 404: diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index fbe7a8b8a0..55802f6a65 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -9,12 +9,15 @@ from django.conf import settings from django.core import signals from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed from django.db import connections, transaction -from django.middleware.exception import ExceptionMiddleware from django.urls import get_resolver, get_urlconf, set_urlconf from django.utils import six from django.utils.deprecation import RemovedInDjango20Warning from django.utils.module_loading import import_string -from django.views import debug + +from .exception import ( + convert_exception_to_response, get_exception_response, + handle_uncaught_exception, +) logger = logging.getLogger('django.request') @@ -48,7 +51,7 @@ class BaseHandler(object): "deprecated. Update your middleware and use settings.MIDDLEWARE " "instead.", RemovedInDjango20Warning ) - handler = self._legacy_get_response + handler = convert_exception_to_response(self._legacy_get_response) for middleware_path in settings.MIDDLEWARE_CLASSES: mw_class = import_string(middleware_path) try: @@ -72,7 +75,7 @@ class BaseHandler(object): if hasattr(mw_instance, 'process_exception'): self._exception_middleware.insert(0, mw_instance.process_exception) else: - handler = self._get_response + handler = convert_exception_to_response(self._get_response) for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) try: @@ -94,10 +97,10 @@ class BaseHandler(object): self._view_middleware.insert(0, mw_instance.process_view) if hasattr(mw_instance, 'process_template_response'): self._template_response_middleware.append(mw_instance.process_template_response) + if hasattr(mw_instance, 'process_exception'): + self._exception_middleware.append(mw_instance.process_exception) - handler = mw_instance - - handler = ExceptionMiddleware(handler, self) + handler = convert_exception_to_response(mw_instance) # We only assign to this when initialization is complete as it is used # as a flag for initialization being complete. @@ -111,25 +114,7 @@ class BaseHandler(object): return view def get_exception_response(self, request, resolver, status_code, exception): - try: - callback, param_dict = resolver.resolve_error_handler(status_code) - # 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.0", - RemovedInDjango20Warning, stacklevel=2 - ) - response = callback(request, **param_dict) - except Exception: - signals.got_request_exception.send(sender=self.__class__, request=request) - response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) - - return response + return get_exception_response(request, resolver, status_code, exception, self.__class__) def get_response(self, request): """Return an HttpResponse object for the given HttpRequest.""" @@ -138,6 +123,8 @@ class BaseHandler(object): response = self._middleware_chain(request) + # This block is only needed for legacy MIDDLEWARE_CLASSES; if + # MIDDLEWARE is used, self._response_middleware will be empty. try: # Apply response middleware, regardless of the response for middleware_method in self._response_middleware: @@ -168,6 +155,11 @@ class BaseHandler(object): return response def _get_response(self, request): + """ + Resolve and call the view, then apply view, exception, and + template_response middleware. This method is everything that happens + inside the request/response middleware. + """ response = None if hasattr(request, 'urlconf'): @@ -237,35 +229,14 @@ class BaseHandler(object): raise def handle_uncaught_exception(self, request, resolver, exc_info): - """ - Processing for any otherwise uncaught exceptions (those that will - generate HTTP 500 responses). Can be overridden by subclasses who want - customised 500 handling. - - Be *very* careful when overriding this because the error could be - caused by anything, so assuming something like the database is always - available would be an error. - """ - if settings.DEBUG_PROPAGATE_EXCEPTIONS: - raise - - logger.error( - 'Internal Server Error: %s', request.path, - exc_info=exc_info, - extra={'status_code': 500, 'request': request}, - ) - - if settings.DEBUG: - return debug.technical_500_response(request, *exc_info) - - # If Http500 handler is not installed, re-raise last exception - if resolver.urlconf_module is None: - six.reraise(*exc_info) - # Return an HttpResponse that displays a friendly error message. - callback, param_dict = resolver.resolve_error_handler(500) - return callback(request, **param_dict) + """Allow subclasses to override uncaught exception handling.""" + return handle_uncaught_exception(request, resolver, exc_info) def _legacy_get_response(self, request): + """ + Apply process_request() middleware and call the main _get_response(), + if needed. Used only for legacy MIDDLEWARE_CLASSES. + """ response = None # Apply request middleware for middleware_method in self._request_middleware: diff --git a/django/core/handlers/exception.py b/django/core/handlers/exception.py new file mode 100644 index 0000000000..9edf457ad1 --- /dev/null +++ b/django/core/handlers/exception.py @@ -0,0 +1,135 @@ +from __future__ import unicode_literals + +import logging +import sys +import warnings +from functools import wraps + +from django.conf import settings +from django.core import signals +from django.core.exceptions import PermissionDenied, SuspiciousOperation +from django.http import Http404 +from django.http.multipartparser import MultiPartParserError +from django.urls import get_resolver, get_urlconf +from django.utils import six +from django.utils.decorators import available_attrs +from django.utils.deprecation import RemovedInDjango20Warning +from django.utils.encoding import force_text +from django.views import debug + +logger = logging.getLogger('django.request') + + +def convert_exception_to_response(get_response): + """ + Wrap the given get_response callable in exception-to-response conversion. + + All exceptions will be converted. All known 4xx exceptions (Http404, + PermissionDenied, MultiPartParserError, SuspiciousOperation) will be + converted to the appropriate response, and all other exceptions will be + converted to 500 responses. + + This decorator is automatically applied to all middleware to ensure that + no middleware leaks an exception and that the next middleware in the stack + can rely on getting a response instead of an exception. + """ + @wraps(get_response, assigned=available_attrs(get_response)) + def inner(request): + try: + response = get_response(request) + except Exception as exc: + response = response_for_exception(request, exc) + return response + return inner + + +def response_for_exception(request, exc): + if isinstance(exc, Http404): + if settings.DEBUG: + response = debug.technical_404_response(request, exc) + else: + response = get_exception_response(request, get_resolver(get_urlconf()), 404, exc) + + elif isinstance(exc, PermissionDenied): + logger.warning( + 'Forbidden (Permission denied): %s', request.path, + extra={'status_code': 403, 'request': request}, + ) + response = get_exception_response(request, get_resolver(get_urlconf()), 403, exc) + + elif isinstance(exc, MultiPartParserError): + logger.warning( + 'Bad request (Unable to parse request body): %s', request.path, + extra={'status_code': 400, 'request': request}, + ) + response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc) + + elif isinstance(exc, SuspiciousOperation): + # The request logger receives events for any problematic request + # The security logger receives events for all SuspiciousOperations + security_logger = logging.getLogger('django.security.%s' % exc.__class__.__name__) + security_logger.error( + force_text(exc), + extra={'status_code': 400, 'request': request}, + ) + if settings.DEBUG: + response = debug.technical_500_response(request, *sys.exc_info(), status_code=400) + else: + response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc) + + elif isinstance(exc, SystemExit): + # Allow sys.exit() to actually exit. See tickets #1023 and #4701 + raise + + else: + signals.got_request_exception.send(sender=None, request=request) + response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info()) + + return response + + +def get_exception_response(request, resolver, status_code, exception, sender=None): + try: + callback, param_dict = resolver.resolve_error_handler(status_code) + # 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.0", + RemovedInDjango20Warning, stacklevel=2 + ) + response = callback(request, **param_dict) + except Exception: + signals.got_request_exception.send(sender=sender, request=request) + response = handle_uncaught_exception(request, resolver, sys.exc_info()) + + return response + + +def handle_uncaught_exception(request, resolver, exc_info): + """ + Processing for any otherwise uncaught exceptions (those that will + generate HTTP 500 responses). + """ + if settings.DEBUG_PROPAGATE_EXCEPTIONS: + raise + + logger.error( + 'Internal Server Error: %s', request.path, + exc_info=exc_info, + extra={'status_code': 500, 'request': request}, + ) + + if settings.DEBUG: + return debug.technical_500_response(request, *exc_info) + + # If Http500 handler is not installed, reraise the last exception. + if resolver.urlconf_module is None: + six.reraise(*exc_info) + # Return an HttpResponse that displays a friendly error message. + callback, param_dict = resolver.resolve_error_handler(500) + return callback(request, **param_dict) diff --git a/django/middleware/exception.py b/django/middleware/exception.py deleted file mode 100644 index 437a82cf26..0000000000 --- a/django/middleware/exception.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import unicode_literals - -import logging -import sys - -from django.conf import settings -from django.core import signals -from django.core.exceptions import PermissionDenied, SuspiciousOperation -from django.http import Http404 -from django.http.multipartparser import MultiPartParserError -from django.urls import get_resolver, get_urlconf -from django.utils.encoding import force_text -from django.views import debug - -logger = logging.getLogger('django.request') - - -class ExceptionMiddleware(object): - """ - Convert selected exceptions to HTTP responses. - - For example, convert Http404 to a 404 response either through handler404 - or through the debug view if settings.DEBUG=True. To ensure that - exceptions raised by other middleware are converted to the appropriate - response, this middleware is always automatically applied as the outermost - middleware. - """ - def __init__(self, get_response, handler=None): - from django.core.handlers.base import BaseHandler - self.get_response = get_response - self.handler = handler or BaseHandler() - - def __call__(self, request): - try: - response = self.get_response(request) - except Http404 as exc: - if settings.DEBUG: - response = debug.technical_404_response(request, exc) - else: - response = self.handler.get_exception_response(request, get_resolver(get_urlconf()), 404, exc) - - except PermissionDenied as exc: - logger.warning( - 'Forbidden (Permission denied): %s', request.path, - extra={'status_code': 403, 'request': request}, - ) - response = self.handler.get_exception_response(request, get_resolver(get_urlconf()), 403, exc) - - except MultiPartParserError as exc: - logger.warning( - 'Bad request (Unable to parse request body): %s', request.path, - extra={'status_code': 400, 'request': request}, - ) - response = self.handler.get_exception_response(request, get_resolver(get_urlconf()), 400, exc) - - except SuspiciousOperation as exc: - # The request logger receives events for any problematic request - # The security logger receives events for all SuspiciousOperations - security_logger = logging.getLogger('django.security.%s' % exc.__class__.__name__) - security_logger.error( - force_text(exc), - extra={'status_code': 400, 'request': request}, - ) - if settings.DEBUG: - return debug.technical_500_response(request, *sys.exc_info(), status_code=400) - - response = self.handler.get_exception_response(request, get_resolver(get_urlconf()), 400, exc) - - except SystemExit: - # Allow sys.exit() to actually exit. See tickets #1023 and #4701 - raise - - except Exception: # Handle everything else. - # Get the exception info now, in case another exception is thrown later. - signals.got_request_exception.send(sender=self.handler.__class__, request=request) - response = self.handler.handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info()) - - return response diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 1a95dbc8b2..65a3893e9a 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -3,13 +3,13 @@ from django.conf import settings from django.conf.urls.i18n import is_language_prefix_patterns_used from django.http import HttpResponseRedirect -from django.middleware.exception import ExceptionMiddleware from django.urls import get_script_prefix, is_valid_path from django.utils import translation from django.utils.cache import patch_vary_headers +from django.utils.deprecation import MiddlewareMixin -class LocaleMiddleware(ExceptionMiddleware): +class LocaleMiddleware(MiddlewareMixin): """ This is a very simple middleware that parses a request and decides what translation object to install in the current @@ -19,17 +19,6 @@ class LocaleMiddleware(ExceptionMiddleware): """ response_redirect_class = HttpResponseRedirect - def __init__(self, get_response=None): - # This override makes get_response optional during the - # MIDDLEWARE_CLASSES deprecation. - super(LocaleMiddleware, self).__init__(get_response) - - def __call__(self, request): - response = self.process_request(request) - if not response: - response = super(LocaleMiddleware, self).__call__(request) - return self.process_response(request, response) - def process_request(self, request): urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf) diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index 7627426080..351bd1d27d 100644 --- a/django/utils/deprecation.py +++ b/django/utils/deprecation.py @@ -121,13 +121,7 @@ class MiddlewareMixin(object): if hasattr(self, 'process_request'): response = self.process_request(request) if not response: - try: - response = self.get_response(request) - except Exception as e: - if hasattr(self, 'process_exception'): - return self.process_exception(request, e) - else: - raise + response = self.get_response(request) if hasattr(self, 'process_response'): response = self.process_response(request, response) return response |
