4

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.

Rui Carvalho
  • 3,376
  • 1
  • 21
  • 18
  • What do you mean by "right logic"? – schillingt May 15 '19 at 14:30
  • Hi @schillingt : I mean, what is the logic I have to put on the `def __call__(self, request):` of the middleware that will short-circuit all underneath middleware classes registered in MIDDLEWARE. So, the 'right logic' means what is the code that makes it skip all process_request and process_response and process_exception of the underneath middleware, but still invoke the proper request/response cycle from the views.py up to that middleware.I know that if I do `return self.get_response(request)` it still calls the process_request of the underneath middleware.That shouldn't happen, but it does. – Rui Carvalho May 16 '19 at 09:46
  • Clarifying better, I want to return a response on the `def __call__(self, request):` that will short-circuit all underneath middleware. But that response needs to be the result of what comes returned from the views.py for that url. Does that make sense? – Rui Carvalho May 16 '19 at 09:51
  • 1
    "So, the 'right logic' means what is the code that makes it skip all process_request and process_response and process_exception of the underneath middleware, but still invoke the proper request/response cycle from the views.py" Django doesn't support that, what you have in your question is likely as good as it'll get for you. – schillingt May 16 '19 at 13:06
  • @schillingt got it! Thanks for the help. Guess I'll stick to my code then. Cheers. – Rui Carvalho May 16 '19 at 16:48

0 Answers0