CONTEXT:
I am working on a Django 1.10/Python2.7 app with legacy stuff, and I am preparing part of it to pay some tech debt in near future. For that I need to put some logic on the chain of middleware used in the app to by-pass a whole set of layers underneath the Django ones if the URL being requested is to hit the API new app (/api
routes).
My idea was to introduce a new middleware in between the Django ones and the custom ones of the project (commented as "Custom middlewares" below as an example of what the proj has - in total about 8 middlewares, some of which make dozens of calls to DB and I don't know yet the implications of removing them or turning them into decorators for requests/views that need them).
That intermediary middleware would short-circuit all the ones below it on MIDDLEWARE
if the url starts with /api
.
I tried short-circuiting the Middleware chain in Django as they say in documentation but it is not working for me.
HOW I GOT IT WORKING (but not ideal):
The way I got it working was by doing this (which is not what they say in the documentation):
This is the pre-existing MIDDLEWARE
chain in settings.py
in project:
(...)
MIDDLEWARE = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'app2.middleware.PreExisting1Middleware', # Custom middlewares
'app3.middleware.PreExisting2Middleware', # Custom middlewares
)
(...)
I added new settings.py
key with:
SHORTCIRCUIT_MIDDLWARES_IF_URL_PATTERNS_IN = (r'^/api', )
To avoid applying PreExisting1Middleware
, PreExisting2Middleware
for URL routes starting with /api
I created a base Middleware class as such:
import re
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import MiddlewareMixin
class ShortCircuitMiddlewareMixin(MiddlewareMixin):
def __call__(self, request):
# Code to be executed for each request before the view (and later middleware) are called.
response = None
if hasattr(self, 'process_request') and self.should_middleware_be_applied(request):
response = self.process_request(request)
if not response:
response = self.get_response(request)
if hasattr(self, 'process_response') and self.should_middleware_be_applied(request):
response = self.process_response(request, response)
return response
def should_middleware_be_applied(self, request):
short_patterns = getattr(settings, 'SHORTCIRCUIT_MIDDLWARES_IF_URL_PATTERNS_IN', [])
if hasattr(short_patterns, '__iter__'):
if any(re.match(pattern, request.path) for pattern in short_patterns):
print('\nShort-circuiting the middleware: {}'.format(self.__class__.__name__))
return False
else:
raise ImproperlyConfigured(
"SHORTCIRCUIT_MIDDLWARES_IF_URL_PATTERNS_IN must be an iterable, got '{}'".format(short_patterns))
return True
Then I use it as a base class for the middlewares PreExisting1Middleware
and PreExisting2Middleware
, that way, they get short-circuited if said url condition holds true.
Class PreExisting1Middleware(ShortCircuitMiddlewareMixin):
def process_request(self, request):
print('\nprocessing...')
(...)
Class PreExisting2Middleware(ShortCircuitMiddlewareMixin):
def process_request(self, request):
(...)
This works just fine, and I get a nice message on shell saying which middlewares were by-passed that can/should be logged if needed as well.
Now, on to the question....
MY QUESTION:
Any idea on how to do it by using the right logic in the __call__
method below and the MIDDLEWARE config below?
class ConditionallyShortcircuitChainMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
(...)
MIDDLEWARE = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'app1.middleware.ConditionallyShortcircuitChainMiddleware', # My new middleware
'app2.middleware.PreExisting1Middleware', # Custom
'app3.middleware.PreExisting2Middleware', # Custom
)
(...)
Relevant info from docs: See here
Middleware order and layering
During the request phase, before calling the view, Django applies middleware in the order it’s defined in MIDDLEWARE, top-down.
You can think of it like an onion: each middleware class is a “layer” that wraps the view, which is in the core of the onion. If the request passes through all the layers of the onion (each one calls get_response to pass the request in to the next layer), all the way to the view at the core, the response will then pass through every layer (in reverse order) on the way back out.
If one of the layers decides to short-circuit and return a response without ever calling its get_response, none of the layers of the onion inside that layer (including the view) will see the request or the response. The response will only return through the same layers that the request passed in through.