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.user directly, it raised AttributeError.

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.

Middleware order decides what request.user isTry it yourself
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

  1. The WSGI or ASGI server calls Django's handler (WSGIHandler / ASGIHandler), which builds an HttpRequest.
  2. The request passes down through MIDDLEWARE in order (the code before get_response()).
  3. The URL resolver matches urls.py patterns and picks the view and its arguments.
  4. Middleware process_view() hooks run (they can return a response early, as CsrfViewMiddleware does).
  5. The view runs and returns an HttpResponse. If it raises, process_exception() hooks run, bottom-up; a TemplateResponse triggers process_template_response() and is rendered.
  6. The response passes back up through the middleware in reverse order.
  7. The server sends it; the request_finished signal 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.user goes below AuthenticationMiddleware.
  • 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 check framework and documentation list ordering requirements; review MIDDLEWARE in 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.