Request Lifecycle: How Web Frameworks Handle a Request · Python Frameworks: Django, Flask and FastAPI
The Django Request Lifecycle and Middleware Order
Why an audit log recorded every action as 'anonymous', and how the order of MIDDLEWARE decides what each middleware can see.
Scenario
Example app: a Django app adds an AuditMiddleware that records who called each endpoint. A developer adds it near the top of MIDDLEWARE so it "captures everything".
What you see in production
- Every audit record says the user is anonymous, even for logged-in staff.
- A security review flags the audit log as useless.
- In a test where the middleware read
request.userdirectly, it raisedAttributeError.
The naive code
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"audit.middleware.AuditMiddleware", # too early
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", # sets request.user
"django.contrib.messages.middleware.MessageMiddleware",
]
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
user = getattr(request, "user", None) # not set yet at this point
audit(request.path, user.username if user else "anonymous")
return self.get_response(request)
Reproduce it
Each Django middleware wraps the rest of the chain: code before get_response() runs top-down on the way in; code after it runs bottom-up on the way out. The simulation runs the same request with the audit middleware in two positions.
class Request:
def __init__(self, path, session_user):
self.path, self.cookies = path, {"sessionid": session_user}
def session_mw(get_response):
def mw(request):
request.session = {"user": request.cookies.get("sessionid")}
return get_response(request)
return mw
def auth_mw(get_response):
def mw(request):
request.user = request.session.get("user") or "AnonymousUser" # requires request.session
return get_response(request)
return mw
def audit_mw(get_response):
def mw(request):
print(f" audit: {request.path} by {getattr(request, 'user', 'anonymous')}")
response = get_response(request)
print(f" audit: response {response}") # way out: bottom-up
return response
return mw
def view(request):
return "200 OK"
def build(middleware):
handler = view
for factory in reversed(middleware): # like Django: the first entry ends up outermost
handler = factory(handler)
return handler
for label, order in [("audit before auth", [audit_mw, session_mw, auth_mw]),
("audit after auth", [session_mw, auth_mw, audit_mw])]:
print(label)
build(order)(Request("/admin/invoices", "staff_ada"))
Output
audit before auth audit: /admin/invoices by anonymous audit: response 200 OK audit after auth audit: /admin/invoices by staff_ada audit: response 200 OK
Root cause
request.user is attached by AuthenticationMiddleware, which itself needs request.session from SessionMiddleware. A middleware listed above them runs its request-phase code before they do, so the user doesn't exist yet.
The lifecycle, step by step
- The WSGI or ASGI server calls Django's handler (
WSGIHandler/ASGIHandler), which builds anHttpRequest. - The request passes down through
MIDDLEWAREin order (the code beforeget_response()). - The URL resolver matches
urls.pypatterns and picks the view and its arguments. - Middleware
process_view()hooks run (they can return a response early, asCsrfViewMiddlewaredoes). - The view runs and returns an
HttpResponse. If it raises,process_exception()hooks run, bottom-up; aTemplateResponsetriggersprocess_template_response()and is rendered. - The response passes back up through the middleware in reverse order.
- The server sends it; the
request_finishedsignal fires (Django closes old database connections here).
See Django's middleware documentation, including the ordering notes for built-in middleware.
The fix
- Place middleware after the middleware it depends on: anything that uses
request.usergoes belowAuthenticationMiddleware. - Consider which phase you need: logging the final status belongs on the way out; blocking requests belongs on the way in.
- For per-view concerns (permissions on a specific view), use decorators or mixins instead of global middleware.
- Write a test that asserts the audit entry for an authenticated request contains the username.
Trade-offs
- Middleware runs for every request (including static files in development and health checks); keep it cheap and skip paths that don't need it.
- Async views and async middleware: mixing sync and async middleware works but adds thread switches; keep the chain consistently async in ASGI deployments for best performance.
Detect it in production
- Check that audit and logging fields aren't constant ("anonymous" for 100% of requests is a red flag).
- Django's
checkframework and documentation list ordering requirements; reviewMIDDLEWAREin code review whenever it changes.
Key points
- Django middleware is an onion: request phase top-down, response phase bottom-up.
- Middleware can only use what earlier middleware set up (
request.session→request.user). - Order: handler → middleware (down) → URL resolver →
process_view→ view →process_exception/template rendering → middleware (up) →request_finished.
Practice questions
Describe the lifecycle of a request in Django.
Answer
The WSGI/ASGI handler builds an HttpRequest; it passes down through MIDDLEWARE in order; the URL resolver picks a view; process_view hooks run; the view returns a response (process_exception on errors, template responses rendered); the response passes back up through middleware in reverse; the request_finished signal fires.
Why does the order of Django's MIDDLEWARE setting matter?
Answer
Each middleware wraps the ones below it. Request-phase code runs top-down and can only use attributes set by earlier middleware (SessionMiddleware before AuthenticationMiddleware, which sets request.user); response-phase code runs bottom-up.
What is process_view in Django middleware?
Answer
A hook called after URL resolution and just before the view, with the view function and its arguments. It can return a response to skip the view, as CsrfViewMiddleware does when the CSRF check fails.
Open in the app to mark lessons complete, take notes and track your progress.