DEV Community

Cover image for A Comprehensive Guide to Django Middleware
Bikramjeet Singh
Bikramjeet Singh

Posted on

A Comprehensive Guide to Django Middleware

What is Middleware?

As the name suggests, middleware is basically a mechanism that comes in the middle of the usual request-response cycle, usually to provide some sort of intermediate functionality. A middleware component takes an HTTP request, performs some operation upon it, and then passes it on to the next component in line, which may be either another middleware or the final view.

Django Middleware Diagram
Image by Jody Boucher

Middleware can also return a response directly instead of forwarding the request further down the chain. For example, Django's CSRF Middleware checks if the request comes from a valid origin (usually the same domain as the server) and immediately returns with a 403 Forbidden response if not. This helps prevent a potentially malicious request from entering the system.

Middleware is always global, that is, it is applied to every request entering the system.

Uses for Middleware

1. Filtering Requests

Middleware can be written to filter out invalid or potentially malicious requests and have them return immediately (usually with an error response), thus blocking them from proceeding further. One contrived example is the CSRF middleware mentioned above, which filters out requests sent from different domains. Another possible example could be an RBAC (Role-Based Access Control) middleware, that prevents users from reading or modifying resources they are not authorised to access. Or, yet another example is geo-blocking middleware that filters out requests from certain geographical locations.

2. Injecting Data into Requests

Middleware can be used to inject additional data into the request that can be used further inside the application. We can take the example of Django's Authentication Middleware, which adds a user object to every valid request. This is a convenient way for the view and other middleware to access details of the logged in user, simply by calling request.user.

3. Performing Logging, Analytics and Other Miscellaneous Tasks

Some middleware don't actually directly modify the request/response at all, but simply make use of the information contained inside them. In other words, these are 'read-only' middleware. For example, imagine an analytics middleware that stores, in a database, the details of all requests entering the system, such as the associated user, the URL, timestamp, etc. This data would later be analysed to identify useful information or trends. Such a middleware would simply read the request content and create/update some related records in the database, then allow it to transparently pass through. Another example would be a usage monitoring middleware that tracks how much of their usage quota a user has exhausted.

Middleware in Django

The structure of a basic middleware in Django looks like this:

class ExampleMiddleware:

    def _init_(self, get_response):
        self.get_response = get_response

    def _call_(self, request):

        # Code that is executed in each request before the view is called

        response = self.get_response(request)

        # Code that is executed in each request after the view is called
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        # This code is executed just before the view is called

    def process_exception(self, request, exception):
        # This code is executed if an exception is raised

    def process_template_response(self, request, response):
        # This code is executed if the response contains a render() method
        return response
Enter fullscreen mode Exit fullscreen mode

The first method, __init__, is the constructor for our Python class. It is called only once, at the time the server starts up. Here is where we perform any initializations or other one-time tasks we may want to do.

As you can see from the code, our constructor has one parameter, the get_response function. This function is passed to our middleware by the Django framework, and its purpose is to pass the request object over to the next middleware, and the get the value of the response.

The second method, __call__ allows an object of our class to be called like a function! That is, it turns it into a callable. This is where we put our actual middleware logic. This method is called by the Django framework to invoke our middleware.

The other three are special 'hook' methods that allow you to invoke your middleware under specific conditions. Note that these are optional, and you can implement them only if you require the functionality they provide. Only the first two methods, __init__ and __call__, are required by the Django framework for your middleware to work properly.

Let's take a look at the hook methods in more detail.

The Django Middleware Hook Methods

process_view

This method is called each time Django receives a request and routes it to a view. How is it different from the __call__ method? Well, here we have access to the view function that Django is routing the request to along with any further arguments (args and kwargs) that will be passed to it. For example, path parameters and their values are usually passed in kwargs, so in case our middleware needs access to them, it must implement the process_view method.

The method must either return None, in which case Django will continue processing the request as usual, or an HttpResponse, in which case it will immediately return with that response.

process_exception

This method is called whenever a view raises an exception that isn't caught within the view itself. Hence, process_exception is invoked after the request has reached and returned from the view.

Similar to the above, the. process_exception method must either return None or an HttpResponse.

process_template_response

This method is also invoked after the view has finished executing. It is only called if the resultant response contains a render() method, which indicates a template is being rendered. You can use this method to alter the content of the template, including its context data, if required.

Registering Middleware

Once you have written your custom middleware class, it has to be registered with your Django project in order to add it to the sequence of middleware each request is passed through. To do this, simply add it as an entry in the MIDDLEWARE list in your main settings.py file:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    .
    .
    .
    # Add your custom middleware to this list
]
Enter fullscreen mode Exit fullscreen mode

The entry should contain the full path to your middleware class, in string form.

Note that the order in which middleware are present in this list is very important. This is because some middleware might depend upon another to function properly, or might even impede the proper functioning of another. Most security-related middleware tend to reside towards the beginning of the list, so they can catch and filter out potentially harmful requests early. As a rule of thumb, most custom middleware are added to the end of the list. However, this is not always true, especially if, as noted above, your custom middleware is seecurity-related. You must decide where to place your middleware entry on a case-by-case basis.

Learning By Practice

Let's create three example middleware classes that will cover all three use-cases we saw earlier. One will keep a record of the number of requests handled and exceptions raised by the server. The second will detect and inject the user agent information into the request. Finally, the last one will filter out requests from certain unsupported user agents.

💡 Note: we'll only be covering the middleware in this practical section, not the setup of the rest of the Django project. If you're unfamiliar with the basics of Django, the official tutorial is a great place to start.

CountRequestsMiddleware

Our CountRequestsMiddleware looks like this:

class CountRequestsMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response
        self.count_requests = 0
        self.count_exceptions = 0

    def __call__(self, request, *args, **kwargs):
        self.count_requests += 1
        logger.info(f"Handled {self.count_requests} requests so far")
        return self.get_response(request)

    def process_exception(self, request, exception):
        self.count_exceptions += 1
        logger.error(f"Encountered {self.count_exceptions} exceptions so far")
Enter fullscreen mode Exit fullscreen mode

In the constructor, we are initializing two variables, count_requests and count_exceptions. In the __call__ method, which is invoked with each request, we are incrementing the value of count_requests and also logging it.

We have also defined the process_exception method, where we similarly increment and log the value of count_exceptions. We are not concerned with the specific type of exception here, although if we wanted to, we could track each type individually by inspecting the value of the exception argument.

We are not defining the other hook methods, since we don't really require them.

SetUserAgentMiddleware

In HTTP, the user agent information is present in the User-Agent header. We will make use of the user-agents Python package to parse this header string and extract meaningful information from it.

class SetUserAgentMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request, *args, **kwargs):
        request.user_agent = user_agents.parse(request.META["HTTP_USER_AGENT"])
        return self.get_response(request)
Enter fullscreen mode Exit fullscreen mode

Here, we're grabbing the value of the user agent header (request.META["HTTP_USER_AGENT"]) and parsing it using the library. Then, we are setting the result as an attribute of the same request object, so that later middlewares and views can access it.

💡 Note: this middleware is basically a reimplimentation of the middleware in the django-user-agents package. While this is fine for learning purposes, in a real-world application always make use of readily available solutions. Don't reinvent the wheel!

BlockMobileMiddleware

Say we're writing a webapp that is only compatible with desktop browsers for some reason (perhaps it's an online game that requires mouse support). We would need some way to detect and block requests that come from mobile browsers. Since we have already written the SetUserAgentMiddleware middleware, we can take advantage of it and simply write another middleware that checks the value of request.user_agent and returns immediately if it is unsupported. This way, we won't have to complicate our view code with user agent checks, since we can guarantee that any request that reaches our it comes from a supported browser.

class BlockMobileMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request, *args, **kwargs):
        if request.user_agent.is_mobile:
            return HttpResponse("Mobile devices are not supported", status=400)
        return self.get_response(request)
Enter fullscreen mode Exit fullscreen mode

Here, we simply check the value of the boolean is_mobile property and, if it is True, immediately return a 400 (Bad Request) response with an appropriate error message.

Note that this depends upon the request.user_agent attribute which is injected by SetUserAgentMiddleware. Therefore, SetUserAgentMiddleware must always come before BlockMobileMiddleware in the middleware list inside your settings file.

Conclusion

I hope you enjoyed this post and found it informative. Please leave a comment below if you spot an error or have something to add.

The complete source code for the example is available on GitHub here.

If you would like to learn more about Django middleware, I cannot recommend the official documentation enough!

Cover image by MustangJoe from Pixabay

Latest comments (5)

Collapse
 
ruslaniv profile image
Ruslan

Great article! Thank you!
Could you give an example how we can process not only the request but also the response using middlewear?

Collapse
 
bikramjeetsingh profile image
Bikramjeet Singh

Sure! If you take a look at the basic structure of a middleware class:

class ExampleMiddleware:

    def _init_(self, get_response):
        self.get_response = get_response

    def _call_(self, request):
        response = self.get_response(request)

        # Code to process response here
        return response
Enter fullscreen mode Exit fullscreen mode

You can see that within the __call__ method, you are getting a response object from Django. You can use this object for whatever functionality you want to implement, and then return it, so that it can continue on to the next middleware in the list. I hope that helps, please let me know if you have any more questions!

Collapse
 
mohammadekhosravi profile image
Mohammad

Thank you for this.

Collapse
 
ericchapman profile image
Eric The Coder

Great post. I learn a lot from it!

Collapse
 
bikramjeetsingh profile image
Bikramjeet Singh

Thank you, I'm glad you found it useful!