Middleware
Introduction#
Middleware in Django is a framework that allows code to hook into the response / request processing and alter the input or output of Django.
Remarks#
Middleware needs to be added to your settings.py MIDDLEWARE_CLASSES
list before it will be included in execution. The default list that Django provides when creating a new project is as follows:
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'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',
]
These are all functions that will run in order on every request (once before it reaches your view code in views.py
and once in reverse order for process_response
callback, before version 1.10). They do a variety of things such as injecting the Cross Site Request Forgery (csrf) token.
The order matters because if some middleware does a redirect, then the all the subsequent middleware will never run. Or if a middleware expects the csrf token to be there, it has to run after the CsrfViewMiddleware
.
Add data to requests
Django makes it really easy to add additional data onto requests for use within the view. For example, we can parse out the subdomain on the request’s META and attach it as a separate property on the request by using middleware.
class SubdomainMiddleware:
def process_request(self, request):
"""
Parse out the subdomain from the request
"""
host = request.META.get('HTTP_HOST', '')
host_s = host.replace('www.', '').split('.')
request.subdomain = None
if len(host_s) > 2:
request.subdomain = host_s[0]
If you add data with middleware to your request, you can access that newly added data further down the line. Here we’ll use the parsed subdomain to determine something like what organization is accessing your application. This approach is useful for apps that are deployed with a DNS setup with wildcard subdomains that all point to a single instance and the person accessing the app wants a skinned version dependent on the access point.
class OrganizationMiddleware:
def process_request(self, request):
"""
Determine the organization based on the subdomain
"""
try:
request.org = Organization.objects.get(domain=request.subdomain)
except Organization.DoesNotExist:
request.org = None
Remember that order matters when having middleware depend on one another. For requests, you’ll want the dependent middleware to be placed after the dependency.
MIDDLEWARE_CLASSES = [
...
'myapp.middleware.SubdomainMiddleware',
'myapp.middleware.OrganizationMiddleware',
...
]
Middleware to filter by IP address
First: The path structure
If you don’t have it you need to create the middleware folder within your app following the structure:
yourproject/yourapp/middleware
The folder middleware should be placed in the same folder as settings.py, urls, templates…
Important: Don’t forget to create the init.py empty file inside the middleware folder so your app recognizes this folder
Instead of having a separate folder containing your middleware classes, it is also possible to put your functions in a single file, yourproject/yourapp/middleware.py
.
Second: Create the middleware
Now we should create a file for our custom middleware. In this example let’s suppose we want a middleware that filter the users based on their IP address, we create a file called filter_ip_middleware.py:
#yourproject/yourapp/middleware/filter_ip_middleware.py
from django.core.exceptions import PermissionDenied
class FilterIPMiddleware(object):
# Check if client IP address is allowed
def process_request(self, request):
allowed_ips = ['192.168.1.1', '123.123.123.123', etc...] # Authorized ip's
ip = request.META.get('REMOTE_ADDR') # Get client IP address
if ip not in allowed_ips:
raise PermissionDenied # If user is not allowed raise Error
# If IP address is allowed we don't do anything
return None
Third: Add the middleware in our ‘settings.py’
We need to look for the MIDDLEWARE_CLASSES
inside the settings.py and there we need to add our middleware (Add it in the last position). It should be like:
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Above are Django standard middlewares
# Now we add here our custom middleware
'yourapp.middleware.filter_ip_middleware.FilterIPMiddleware'
)
Done! Now every request from every client will call your custom middleware and process your custom code!
Globally handling exception
Say you have implemented some logic to detect attempts to modify an object in the database while the client that submitted changes didn’t have the latest modifications. If such case happens, you raise a custom exception ConfictError(detailed_message)
.
Now you want to return an HTTP 409 (Confict) status code when this error occurs. You may typically use as middleware for this instead of handling it in each view that might raise this exception.
class ConfictErrorHandlingMiddleware:
def process_exception(self, request, exception):
if not isinstance(exception, ConflictError):
return # Propagate other exceptions, we only handle ConflictError
context = dict(confict_details=str(exception))
return TemplateResponse(request, '409.html', context, status=409)
Understanding Django 1.10 middleware’s new style
Django 1.10 introduced a new middleware style where process_request
and process_response
are merged together.
In this new style, a middleware is a callable that returns another callable. Well, actually the former is a middleware factory and the latter is the actual middleware.
The middleware factory takes as single argument the next middleware in the middlewares stack, or the view itself when the bottom of the stack is reached.
The middleware takes the request as single argument and always returns an HttpResponse
.
The best example to illustrate how new-style middleware works is probably to show how to make a backward-compatible middleware:
class MyMiddleware:
def __init__(self, next_layer=None):
"""We allow next_layer to be None because old-style middlewares
won't accept any argument.
"""
self.get_response = next_layer
def process_request(self, request):
"""Let's handle old-style request processing here, as usual."""
# Do something with request
# Probably return None
# Or return an HttpResponse in some cases
def process_response(self, request, response):
"""Let's handle old-style response processing here, as usual."""
# Do something with response, possibly using request.
return response
def __call__(self, request):
"""Handle new-style middleware here."""
response = self.process_request(request)
if response is None:
# If process_request returned None, we must call the next middleware or
# the view. Note that here, we are sure that self.get_response is not
# None because this method is executed only in new-style middlewares.
response = self.get_response(request)
response = self.process_response(request, response)
return response