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

Request Flow Through Middleware

text
Request → SecurityMiddleware → SessionMiddleware → CommonMiddleware
       → CsrfViewMiddleware → AuthenticationMiddleware → … → View
       → (response travels back through middleware in reverse)

Default MIDDLEWARE in a new Django 5 project (simplified):

python
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
  • SecurityMiddleware sets 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:

python
MIDDLEWARE = [
    ...
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "blog.middleware.RequestTimingMiddleware",
]

Code explanation:

  • get_response is the next middleware or view callable
  • New-style middleware uses a single __call__ (Django 1.10+)
  • Logs appear once LOGGING is configured — Error Handling and Logging

Maintenance Mode Middleware

blog/middleware.py (add):

settings/production.py:

python
MAINTENANCE_MODE = os.environ.get("MAINTENANCE_MODE", "false").lower() == "true"

Allow staff through admin during deploys.

Middleware vs Flask Hooks

DjangoFlaskNotes
Middleware.__call__@app.before_requestDjango wraps both directions in one class
Response mutation@app.after_requestReturn modified response in Django __call__ after get_response
Per-request storageflask.gAttach to request attributes (avoid collisions)

Signals Overview

Signals decouple side effects from core save logic:

SignalFires when
pre_saveBefore model save()
post_saveAfter save (create or update)
pre_deleteBefore delete
post_deleteAfter delete
m2m_changedM2M relation changes

Connect in blog/apps.py (recommended):

python
from django.apps import AppConfig
 
 
class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"
 
    def ready(self):
        import blog.signals  # noqa: F401

config/settings/base.py — use app config class:

python
INSTALLED_APPS = [
    ...
    "blog.apps.BlogConfig",
]

Example: post_save Signal

blog/signals.py:

Code explanation:

  • created is True only on insert
  • Avoid calling instance.save() inside post_save without 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:

python
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 signalsPrefer explicit code
Audit logging on every saveCheckout payment flow
Search index updatesComplex validation
Denormalized counter updatesMulti-step transactions

Post-Chapter Checklist

  • RequestTimingMiddleware registered and logs one line per request
  • You can explain default middleware order (session → auth)
  • BlogConfig.ready() imports blog.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.