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.
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
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
]
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")
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)
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)
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
Top comments (6)
Great article! Thank you!
Could you give an example how we can process not only the request but also the response using middlewear?
Sure! If you take a look at the basic structure of a middleware class:
You can see that within the
__call__
method, you are getting aresponse
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!Thank you for this.
Great post. I learn a lot from it!
Thank you, I'm glad you found it useful!
Thanks. Very helpful. Just to notice, You just missed the double underscores in ExampleMiddleware code.