Refs #26601 -- Refactored BaseHandler to prepare for new-style middleware.

This commit is contained in:
Florian Apolloner 2016-05-02 12:37:49 -04:00 committed by Tim Graham
parent c999c8d8f6
commit 05c888ffb8
2 changed files with 164 additions and 126 deletions

View File

@ -5,18 +5,14 @@ import sys
import types import types
import warnings import warnings
from django import http
from django.conf import settings from django.conf import settings
from django.core import signals from django.core import signals
from django.core.exceptions import ( from django.core.exceptions import MiddlewareNotUsed
MiddlewareNotUsed, PermissionDenied, SuspiciousOperation,
)
from django.db import connections, transaction from django.db import connections, transaction
from django.http.multipartparser import MultiPartParserError from django.middleware.exception import ExceptionMiddleware
from django.urls import get_resolver, set_urlconf from django.urls import get_resolver, get_urlconf, set_urlconf
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
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
@ -31,6 +27,7 @@ class BaseHandler(object):
self._template_response_middleware = None self._template_response_middleware = None
self._response_middleware = None self._response_middleware = None
self._exception_middleware = None self._exception_middleware = None
self._middleware_chain = None
def load_middleware(self): def load_middleware(self):
""" """
@ -38,12 +35,13 @@ class BaseHandler(object):
Must be called after the environment is fixed (see __call__ in subclasses). Must be called after the environment is fixed (see __call__ in subclasses).
""" """
self._request_middleware = []
self._view_middleware = [] self._view_middleware = []
self._template_response_middleware = [] self._template_response_middleware = []
self._response_middleware = [] self._response_middleware = []
self._exception_middleware = [] self._exception_middleware = []
request_middleware = [] handler = self._legacy_get_response
for middleware_path in settings.MIDDLEWARE_CLASSES: for middleware_path in settings.MIDDLEWARE_CLASSES:
mw_class = import_string(middleware_path) mw_class = import_string(middleware_path)
try: try:
@ -57,7 +55,7 @@ class BaseHandler(object):
continue continue
if hasattr(mw_instance, 'process_request'): if hasattr(mw_instance, 'process_request'):
request_middleware.append(mw_instance.process_request) self._request_middleware.append(mw_instance.process_request)
if hasattr(mw_instance, 'process_view'): if hasattr(mw_instance, 'process_view'):
self._view_middleware.append(mw_instance.process_view) self._view_middleware.append(mw_instance.process_view)
if hasattr(mw_instance, 'process_template_response'): if hasattr(mw_instance, 'process_template_response'):
@ -67,9 +65,11 @@ class BaseHandler(object):
if hasattr(mw_instance, 'process_exception'): if hasattr(mw_instance, 'process_exception'):
self._exception_middleware.insert(0, mw_instance.process_exception) self._exception_middleware.insert(0, mw_instance.process_exception)
handler = ExceptionMiddleware(handler, self)
# We only assign to this when initialization is complete as it is used # We only assign to this when initialization is complete as it is used
# as a flag for initialization being complete. # as a flag for initialization being complete.
self._request_middleware = request_middleware self._middleware_chain = handler
def make_view_atomic(self, view): def make_view_atomic(self, view):
non_atomic_requests = getattr(view, '_non_atomic_requests', set()) non_atomic_requests = getattr(view, '_non_atomic_requests', set())
@ -100,121 +100,11 @@ class BaseHandler(object):
return response return response
def get_response(self, request): def get_response(self, request):
"Returns an HttpResponse object for the given HttpRequest" """Return an HttpResponse object for the given HttpRequest."""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
# Setup default url resolver for this thread, this code is outside response = self._middleware_chain(request)
# the try/except so we don't get a spurious "unbound local
# variable" exception in the event an exception is raised before
# resolver is set
urlconf = settings.ROOT_URLCONF
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
# Use a flag to check if the response was rendered to prevent
# multiple renderings or to force rendering if necessary.
response_is_rendered = False
try:
response = None
# Apply request middleware
for middleware_method in self._request_middleware:
response = middleware_method(request)
if response:
break
if response is None:
if hasattr(request, 'urlconf'):
# Reset url resolver with a custom URLconf.
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
# Apply view middleware
for middleware_method in self._view_middleware:
response = middleware_method(request, callback, callback_args, callback_kwargs)
if response:
break
if response is None:
wrapped_callback = self.make_view_atomic(callback)
try:
response = wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e:
response = self.process_exception_by_middleware(e, request)
# Complain if the view returned None (a common error).
if response is None:
if isinstance(callback, types.FunctionType): # FBV
view_name = callback.__name__
else: # CBV
view_name = callback.__class__.__name__ + '.__call__'
raise ValueError(
"The view %s.%s didn't return an HttpResponse object. It "
"returned None instead." % (callback.__module__, view_name)
)
# If the response supports deferred rendering, apply template
# response middleware and then render the response
if hasattr(response, 'render') and callable(response.render):
for middleware_method in self._template_response_middleware:
response = middleware_method(request, response)
# Complain if the template response middleware returned None (a common error).
if response is None:
raise ValueError(
"%s.process_template_response didn't return an "
"HttpResponse object. It returned None instead."
% (middleware_method.__self__.__class__.__name__)
)
try:
response = response.render()
except Exception as e:
response = self.process_exception_by_middleware(e, request)
response_is_rendered = True
except http.Http404 as exc:
if settings.DEBUG:
response = debug.technical_404_response(request, exc)
else:
response = self.get_exception_response(request, resolver, 404, exc)
except PermissionDenied as exc:
logger.warning(
'Forbidden (Permission denied): %s', request.path,
extra={'status_code': 403, 'request': request},
)
response = self.get_exception_response(request, resolver, 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.get_exception_response(request, resolver, 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.get_exception_response(request, resolver, 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.__class__, request=request)
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
try: try:
# Apply response middleware, regardless of the response # Apply response middleware, regardless of the response
@ -228,13 +118,13 @@ class BaseHandler(object):
% (middleware_method.__self__.__class__.__name__)) % (middleware_method.__self__.__class__.__name__))
except Exception: # Any exception should be gathered and handled except Exception: # Any exception should be gathered and handled
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, get_resolver(get_urlconf()), sys.exc_info())
response._closable_objects.append(request) response._closable_objects.append(request)
# If the exception handler returns a TemplateResponse that has not # If the exception handler returns a TemplateResponse that has not
# been rendered, force it to be rendered. # been rendered, force it to be rendered.
if not response_is_rendered and callable(getattr(response, 'render', None)): if not getattr(response, 'is_rendered', True) and callable(getattr(response, 'render', None)):
response = response.render() response = response.render()
if response.status_code == 404: if response.status_code == 404:
@ -245,6 +135,64 @@ class BaseHandler(object):
return response return response
def _get_response(self, request):
response = None
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
# Apply view middleware
for middleware_method in self._view_middleware:
response = middleware_method(request, callback, callback_args, callback_kwargs)
if response:
return response
wrapped_callback = self.make_view_atomic(callback)
try:
response = wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e:
response = self.process_exception_by_middleware(e, request)
# Complain if the view returned None (a common error).
if response is None:
if isinstance(callback, types.FunctionType): # FBV
view_name = callback.__name__
else: # CBV
view_name = callback.__class__.__name__ + '.__call__'
raise ValueError(
"The view %s.%s didn't return an HttpResponse object. It "
"returned None instead." % (callback.__module__, view_name)
)
# If the response supports deferred rendering, apply template
# response middleware and then render the response
elif hasattr(response, 'render') and callable(response.render):
for middleware_method in self._template_response_middleware:
response = middleware_method(request, response)
# Complain if the template response middleware returned None (a common error).
if response is None:
raise ValueError(
"%s.process_template_response didn't return an "
"HttpResponse object. It returned None instead."
% (middleware_method.__self__.__class__.__name__)
)
try:
response = response.render()
except Exception as e:
response = self.process_exception_by_middleware(e, request)
return response
def process_exception_by_middleware(self, exception, request): def process_exception_by_middleware(self, exception, request):
""" """
Pass the exception to the exception middleware. If no middleware Pass the exception to the exception middleware. If no middleware
@ -284,3 +232,15 @@ class BaseHandler(object):
# Return an HttpResponse that displays a friendly error message. # Return an HttpResponse that displays a friendly error message.
callback, param_dict = resolver.resolve_error_handler(500) callback, param_dict = resolver.resolve_error_handler(500)
return callback(request, **param_dict) return callback(request, **param_dict)
def _legacy_get_response(self, request):
response = None
# Apply request middleware
for middleware_method in self._request_middleware:
response = middleware_method(request)
if response:
break
if response is None:
response = self._get_response(request)
return response

View File

@ -0,0 +1,78 @@
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