Middleware and Signals
Introduction
Middleware wraps every HTTP request and response—security headers, sessions, authentication, and your custom logic run in a defined order before views execute. Signals let apps react to events like post_save without tangling business logic inside models. This chapter builds a request-timing middleware, a maintenance-mode gate, and a signal that syncs slug defaults—compared to Flask hooks.
Prerequisites
- Settings —
MIDDLEWARElist - User Authentication —
AuthenticationMiddleware - Models and ORM Basics
Request Flow Through Middleware
Request → SecurityMiddleware → SessionMiddleware → CommonMiddleware
→ CsrfViewMiddleware → AuthenticationMiddleware → … → View
→ (response travels back through middleware in reverse)Default MIDDLEWARE in a new Django 5 project (simplified):
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",
]Code explanation:
- Order matters—sessions before auth; CSRF before views that accept POST
SecurityMiddlewaresets security headers when configured in settings
Tip
Do Not Reorder Middleware Casually
Moving AuthenticationMiddleware before SessionMiddleware breaks request.user. Add custom middleware after Django's session and auth stack unless you know the implications.
Custom Middleware: Request Timing
Create blog/middleware.py:
Register in settings/base.py:
MIDDLEWARE = [
...
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"blog.middleware.RequestTimingMiddleware",
]Code explanation:
get_responseis the next middleware or view callable- New-style middleware uses a single
__call__(Django 1.10+) - Logs appear once
LOGGINGis configured — Error Handling and Logging
Maintenance Mode Middleware
blog/middleware.py (add):
settings/production.py:
MAINTENANCE_MODE = os.environ.get("MAINTENANCE_MODE", "false").lower() == "true"Allow staff through admin during deploys.
Middleware vs Flask Hooks
| Django | Flask | Notes |
|---|---|---|
Middleware.__call__ | @app.before_request | Django wraps both directions in one class |
| Response mutation | @app.after_request | Return modified response in Django __call__ after get_response |
| Per-request storage | flask.g | Attach to request attributes (avoid collisions) |
Signals Overview
Signals decouple side effects from core save logic:
| Signal | Fires when |
|---|---|
pre_save | Before model save() |
post_save | After save (create or update) |
pre_delete | Before delete |
post_delete | After delete |
m2m_changed | M2M relation changes |
Connect in blog/apps.py (recommended):
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
import blog.signals # noqa: F401config/settings/base.py — use app config class:
INSTALLED_APPS = [
...
"blog.apps.BlogConfig",
]Example: post_save Signal
blog/signals.py:
Code explanation:
createdisTrueonly on insert- Avoid calling
instance.save()insidepost_savewithout guard—can recurse;update()is safer here
Warning
Signals Can Hide Logic
Too many signals make debugging hard ("who changed this field?"). Prefer explicit service functions for critical business rules; use signals for cross-cutting concerns (cache invalidation, audit logs).
m2m_changed Example (Awareness)
Invalidate a cache key when tags change:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Post
@receiver(m2m_changed, sender=Post.tags.through)
def tags_changed(sender, instance, action, **kwargs):
if action in ("post_add", "post_remove", "post_clear"):
pass # cache.delete(f"post:{instance.pk}")When Not to Use Signals
| Prefer signals | Prefer explicit code |
|---|---|
| Audit logging on every save | Checkout payment flow |
| Search index updates | Complex validation |
| Denormalized counter updates | Multi-step transactions |
Post-Chapter Checklist
-
RequestTimingMiddlewareregistered and logs one line per request - You can explain default middleware order (session → auth)
-
BlogConfig.ready()importsblog.signals - You know signal recursion pitfalls
FAQ
Old-style middleware with process_request?
Deprecated—use __call__ pattern shown above.
Async middleware?
Django supports async def __call__ for ASGI—advanced; this track focuses on WSGI.
Disable CSRF for one view?
@csrf_exempt on the view—not middleware removal.
Signal not firing?
Check AppConfig.ready(), correct sender, and that code path calls save().
Compare to Spring @EventListener?
Similar decoupling idea—Spring Boot events vs Django signals.