class LoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print(f"Incoming: {request.path}")
response = self.get_response(request) # pass control down the chain
print(f"Outgoing: {response.status_code}")
return response
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"myapp.middleware.LoggingMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
]
Django makes the "wrapping" nature the most explicit of the three: each middleware is a callable that wraps get_response (effectively, "everything after me"), calls it, and can act both before and after that call — which is precisely the down-then-up shape all three frameworks share. Exactly like ASP.NET Core's class-based middleware, __init__ runs once at server startup (where get_response — the next layer down — is captured), and __call__ runs on every single request.
process_view, process_exception, and the older hook-based styleModern Django middleware is written as the single __call__ method above, but a middleware class can additionally define optional hook methods that Django calls at specific points, without needing to restructure the whole __call__:
class AdminOnlyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_view(self, request, view_func, view_args, view_kwargs):
# runs right before Django calls the view, after URL routing has resolved it —
# this is the only point where the middleware knows WHICH view is about to run
if request.path.startswith("/admin/") and not request.user.is_staff:
return HttpResponseForbidden()
def process_exception(self, request, exception):
# runs if the view raised an unhandled exception
log_exception(exception)
return None # returning None lets Django's normal exception handling continue
process_view exists because, by the time __call__ itself runs, Django hasn't resolved the URL to a specific view yet — a middleware that needs to know which view is about to handle the request (not just the raw path) has no other hook for that information.
class MaintenanceModeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if settings.MAINTENANCE_MODE:
return HttpResponse("Down for maintenance", status=503) # get_response is never called
return self.get_response(request)
Short-circuiting in Django is simply not calling self.get_response(request) and returning a response directly instead — there's no special API for it, unlike Express's four-argument error handlers or ASP.NET Core's terminal app.Run. This is arguably the simplest of the three mechanisms to reason about, precisely because it's just an if statement around a function call.