Skip to main content

Django: Creating Custom Middleware

Django: Creating Custom Middleware

Custom middleware in Django allows developers to create reusable components that process HTTP requests and responses, extending the framework’s built-in functionality. Integrated into Django’s request-response cycle, custom middleware enables tasks like request logging, authentication checks, or response modification. This tutorial explores creating custom middleware in Django, covering setup, implementation, performance considerations, and practical use cases for applications like e-commerce or content platforms.


01. What Is Custom Middleware?

Middleware in Django is a layer that processes requests before they reach views and responses before they leave the server. Custom middleware is user-defined code that hooks into this cycle to perform specific tasks, such as adding headers, tracking metrics, or enforcing custom authentication. It’s ideal for cross-cutting concerns in monolithic or microservices-based Django applications, ensuring modularity and reusability.

Example: Setting Up a Django Project for Custom Middleware

# Install Django
pip install django

# Create a Django project
django-admin startproject custom_middleware

# Navigate to project directory
cd custom_middleware

# Create an app
python manage.py startapp core

# Run the development server
python manage.py runserver

Output:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 15, 2025 - 22:39:00
Django version 4.2, using settings 'custom_middleware.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Explanation:

  • startproject - Initializes a Django project for custom middleware.
  • The core app will house middleware logic.

02. Core Concepts of Custom Middleware

Custom middleware is created as a Python class or function that processes requests, responses, or exceptions. It’s added to the MIDDLEWARE setting to integrate with Django’s request-response pipeline. Below is a summary of key concepts and their roles:

Concept Description Use Case
Middleware Class Class with methods like process_request Handle request/response lifecycle
Function-based Middleware Simple callable for quick tasks Lightweight processing
Middleware Order Position in MIDDLEWARE list matters Control execution sequence
Performance Minimize overhead in middleware Ensure scalability


2.1 Creating a Class-based Middleware

Example: Logging Request Middleware

# core/middleware.py
import logging

logger = logging.getLogger(__name__)

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

    def __call__(self, request):
        logger.info(f"Request: {request.method} {request.path}")
        response = self.get_response(request)
        logger.info(f"Response: {response.status_code}")
        return response

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.RequestLoggingMiddleware',
]

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        '': {
            'handlers': ['console'],
            'level': 'INFO',
        },
    },
}

# core/views.py
from django.http import HttpResponse

def home(request):
    return HttpResponse("Welcome to the site!")

# custom_middleware/urls.py
from django.contrib import admin
from django.urls import path
from core.views import home

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', home, name='home'),
]

Output:

Request: GET /
Response: 200

Explanation:

  • __call__ - Processes each request and response.
  • Logs request method, path, and response status for debugging.

2.2 Function-based Middleware

Example: Adding Custom Header

# core/middleware.py
def custom_header_middleware(get_response):
    def middleware(request):
        response = get_response(request)
        response['X-Custom-Header'] = 'MyApp'
        return response
    return middleware

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.custom_header_middleware',
]

Output:

Response Headers: X-Custom-Header: MyApp

Explanation:

  • Function-based middleware is simpler for lightweight tasks.
  • Adds a custom header to every response.

2.3 Handling Exceptions

Example: Exception Handling Middleware

# core/middleware.py
from django.http import JsonResponse

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

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        return JsonResponse({
            'error': str(exception),
            'path': request.path
        }, status=500)

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.ExceptionHandlingMiddleware',
]

# core/views.py
def error_view(request):
    raise ValueError("Test error")

Output:

{"error": "Test error", "path": "/error/"}

Explanation:

  • process_exception - Catches unhandled exceptions.
  • Returns a JSON response for better API error handling.

2.4 Incorrect Middleware Implementation

Example: Blocking Middleware

# core/middleware.py (Incorrect)
import time

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

    def __call__(self, request):
        time.sleep(2)  # Simulate heavy processing
        response = self.get_response(request)
        return response

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.SlowMiddleware',
]

Output:

Request delayed by 2 seconds

Explanation:

  • Heavy processing in middleware slows down all requests.
  • Solution: Offload heavy tasks to asynchronous workers like Celery.

03. Effective Usage

3.1 Recommended Practices

  • Keep middleware lightweight to avoid performance bottlenecks.

Example: Lightweight API Key Middleware

# core/middleware.py
from django.http import JsonResponse

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

    def __call__(self, request):
        api_key = request.headers.get('X-API-Key')
        if api_key != 'secret-key':
            return JsonResponse({'error': 'Invalid API key'}, status=401)
        return self.get_response(request)

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.APIKeyMiddleware',
]

Output:

{"error": "Invalid API key"}  # If key is missing or incorrect
  • Simple header check ensures fast execution.
  • Secures API endpoints without heavy logic.

3.2 Practices to Avoid

  • Avoid modifying requests or responses unnecessarily.

Example: Overwriting Response Content

# core/middleware.py (Incorrect)
class OverwriteMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        response.content = b"Overwritten content"  # Overrides all responses
        return response

Output:

Overwritten content  # Breaks intended response
  • Overwriting content can disrupt views or APIs.
  • Solution: Modify responses only when necessary and with clear intent.

04. Common Use Cases

4.1 Request Rate Limiting

Limit requests to prevent abuse in an API.

Example: Rate Limiting Middleware

# core/middleware.py
from django.core.cache import cache
from django.http import JsonResponse

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

    def __call__(self, request):
        client_ip = request.META.get('REMOTE_ADDR')
        key = f"rate_limit:{client_ip}"
        request_count = cache.get(key, 0)
        if request_count >= 100:  # 100 requests per minute
            return JsonResponse({'error': 'Rate limit exceeded'}, status=429)
        cache.set(key, request_count + 1, timeout=60)
        return self.get_response(request)

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.RateLimitMiddleware',
]

Output:

{"error": "Rate limit exceeded"}  # After 100 requests

Explanation:

  • Uses cache to track request counts per IP.
  • Prevents abuse without affecting legitimate users.

4.2 Custom Authentication

Enforce token-based authentication for microservices.

Example: Token Authentication Middleware

# core/middleware.py
from django.http import JsonResponse

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

    def __call__(self, request):
        token = request.headers.get('Authorization')
        if token != 'Bearer valid-token':
            return JsonResponse({'error': 'Invalid token'}, status=401)
        return self.get_response(request)

# custom_middleware/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'core.middleware.TokenAuthMiddleware',
]

Output:

{"error": "Invalid token"}  # If token is missing or invalid

Explanation:

  • Validates tokens for secure service communication.
  • Lightweight check suitable for microservices.

Conclusion

Custom middleware in Django empowers developers to extend the request-response cycle with application-specific logic, enhancing flexibility and modularity. By creating lightweight, well-structured middleware, you can address cross-cutting concerns efficiently. Key takeaways:

  • Use class-based middleware for complex logic, function-based for simple tasks.
  • Handle exceptions with process_exception.
  • Keep middleware lightweight to maintain performance.
  • Avoid heavy processing or unnecessary response modifications.

With custom middleware, Django enables you to build scalable, maintainable web applications!

Comments