Middleware

Reviewed & published by Brayan K

Middleware in Django is a layer of code that wraps every request and response — each middleware sees the request on the way in and the response on the way out, letting you add cross-cutting behaviour like authentication, logging, or security headers in one place instead of in every view.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn how the request/response cycle flows through the middleware stack, how to write your own middleware class, and which hooks and built-in middleware Django gives you out of the box.

What You'll Learn in This Lesson

1 The Request/Response Cycle and the MIDDLEWARE Setting

When a request arrives, Django does not hand it straight to your view. It first passes the request through a stack of middleware — small components listed in the MIDDLEWARE setting. The request travels down the stack to reach the view, and the response travels back up the stack to reach the browser.

This is the onion model: the first middleware in the list is the outermost layer. It sees the request first and the response last. The last middleware is the innermost layer, closest to the view.

A default settings.py already lists several middleware. The order is deliberate — for example, security checks should wrap everything, and session handling must run before authentication because authentication reads the session.

# settings.py — the order in this list IS the onion order.
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",        # outermost
    "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",  # innermost
]
# Worked Example: the onion model in plain Python.
# Each middleware is a callable class that wraps the next one.
# We chain two middleware around a fake "view" and watch the order.

def view(request):
    print("  VIEW: handling", request["path"])
    return {"status": 200, "body": "Hello"}

class LoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response          # store the next layer

    def __call__(self, request):
        print("Logging   -> request in")          # before (on the way IN)
        response = self.get_response(request)      # go deeper into the onion
        print("Logging   <- response out")        # after (on the way OUT)
        return response

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print("  Timing  -> request in")
        response = self.get_response(request)
        print("  Timing  <- response out")
        return response

# Build the onion: Logging wraps Timing wraps the view.
handler = LoggingMiddleware(TimingMiddleware(view))
result = handler({"path": "/home"})
print("Final response:", result)

# Notice: requests go IN top-to-bottom, responses come OUT bottom-to-top.

# ✅ Expected output:
# Logging   -> request in
#   Timing  -> request in
#   VIEW: handling /home
#   Timing  <- response out
# Logging   <- response out
# Final response: {'status': 200, 'body': 'Hello'}

2 Writing Custom Middleware as a Class

The modern way to write middleware is a class with two methods. __init__(self, get_response) runs once when the server boots and stores the get_response callable — the next layer of the onion. __call__(self, request) runs for every request.

Inside __call__ you write code before calling get_response(request) (the request phase) and code after it returns (the response phase). You must always return the response at the end.

# myapp/middleware.py — a real Django middleware class.
import time

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response   # called ONCE at startup

    def __call__(self, request):
        start = time.monotonic()           # BEFORE: request phase

        response = self.get_response(request)   # call the view (and inner layers)

        duration = time.monotonic() - start     # AFTER: response phase
        response["X-Render-Time"] = f"{duration:.4f}s"
        return response                    # always return the response!

To activate it, add its dotted path to the MIDDLEWARE list:

# settings.py
MIDDLEWARE = [
    # ... the built-in middleware ...
    "myapp.middleware.TimingMiddleware",
]
# Worked Example: a custom middleware that runs code before AND after.
# This is the exact __init__ / __call__ shape Django expects, runnable here.

def view(request):
    return {"status": 200, "headers": {}, "body": "OK"}

class HeaderMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # BEFORE: inspect/modify the incoming request
        request["received"] = True
        print("before: request marked as received")

        response = self.get_response(request)   # hand off to the view

        # AFTER: inspect/modify the outgoing response
        response["headers"]["X-Powered-By"] = "Django"
        print("after: added X-Powered-By header")
        return response

handler = HeaderMiddleware(view)
result = handler({"path": "/"})
print("Response headers:", result["headers"])
print("Request was marked received:", True)

# ✅ Expected output:
# before: request marked as received
# after: added X-Powered-By header
# Response headers: {'X-Powered-By': 'Django'}
# Request was marked received: True

3 Hooks and Built-in Middleware

Beyond __call__, middleware can define optional hook methods. Django calls them automatically at specific points in the cycle if they exist:

# myapp/middleware.py — using hook methods alongside __call__.
import logging

logger = logging.getLogger("myapp")

class AuditMiddleware:
    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):
        logger.info("About to run view: %s", view_func.__name__)
        return None   # return None to let Django continue normally

    def process_exception(self, request, exception):
        logger.error("View raised: %r", exception)
        return None   # return None to let Django's normal error handling run

Django ships with many built-in middleware so common needs are handled for you:

# Worked Example: simulate process_view and process_exception hooks.
# Django calls these extra methods around the view automatically.

class GuardMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Simulate Django's flow: process_view, then the view.
        self.process_view(request, request["view"])
        try:
            return request["view"](request)
        except Exception as exc:
            handled = self.process_exception(request, exc)
            return handled if handled else {"status": 500}

    def process_view(self, request, view_func):
        print("process_view: about to call", view_func.__name__)

    def process_exception(self, request, exception):
        print("process_exception caught:", repr(exception))
        return {"status": 500, "body": "Friendly error page"}

def boom(request):
    raise ValueError("something broke")

mw = GuardMiddleware(get_response=None)
result = mw({"path": "/boom", "view": boom})
print("Final response:", result)

# ✅ Expected output:
# process_view: about to call boom
# process_exception caught: ValueError('something broke')
# Final response: {'status': 500, 'body': 'Friendly error page'}

🎯 Your Turn: Complete the Middleware

Fill in the blank so the middleware correctly calls the next layer. The __call__ method must call get_response(request) and return its result.

# 🎯 YOUR TURN: replace ___ with the call that hands off to the next layer.
# Hint: it's self.get_response(request)

def view(request):
    return {"status": 200, "body": "Welcome"}

class CounterMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.count = 0

    def __call__(self, request):
        self.count += 1
        print("Handling request number", self.count)
        response = ___          # <-- call the next layer here
        return response

handler = CounterMiddleware(view)
print(handler({"path": "/"}))
print(handler({"path": "/about"}))

# EXPECTED OUTPUT:
# Handling request number 1
# {'status': 200, 'body': 'Welcome'}
# Handling request number 2
# {'status': 200, 'body': 'Welcome'}

Common Errors (And How to Fix Them)

❌ Forgetting to return the response from __call__

If __call__ calls get_response(request) but does not return it, Django gets None and raises a "didn't return an HttpResponse object" error.

✅ Fix: always end __call__ with return response.

❌ Not calling get_response(request) at all

If your middleware never calls get_response(request), the inner layers and the view never run, so no real response is produced.

✅ Fix: call response = self.get_response(request) unless you intentionally short-circuit with your own response.

❌ Wrong order in the MIDDLEWARE list

Placing AuthenticationMiddleware before SessionMiddleware breaks request.user, because auth reads the session that hasn't been attached yet.

✅ Fix: keep SessionMiddleware above AuthenticationMiddleware and follow Django's documented default order.

📋 Quick Reference

NameWhat It Does
MIDDLEWAREThe ordered list in settings.py that defines the onion stack
__init__(self, get_response)Runs once at startup; stores the next layer's callable
__call__(self, request)Runs per request; code before and after get_response
process_viewHook called just before the view runs
process_exceptionHook called when a view raises an unhandled exception

🏆 Mini Challenge: Block Middleware

Write middleware that short-circuits the onion: if the request path is blocked, return a 403 response without calling the view. Otherwise call get_response as normal.

# 🏆 Mini Challenge: short-circuit the request when the path is blocked.

def view(request):
    return {"status": 200, "body": "Secret page"}

class BlockMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.blocked = {"/admin-secret"}

    def __call__(self, request):
        if request["path"] in self.blocked:
            # Short-circuit: return early, do NOT call get_response.
            return {"status": 403, "body": "Forbidden"}
        return self.get_response(request)

handler = BlockMiddleware(view)
print(handler({"path": "/home"}))
print(handler({"path": "/admin-secret"}))

# EXPECTED:
# {'status': 200, 'body': 'Secret page'}
# {'status': 403, 'body': 'Forbidden'}

❓ Frequently Asked Questions

Lesson complete — you understand the middleware stack!

You now know how the request/response cycle flows through the onion of middleware, how to write a custom middleware class with __init__ and __call__, how to use hooks like process_view and process_exception, and which built-in middleware Django provides.

Practice quiz

Where does middleware sit in the request flow?

  • Between the web server and your views
  • Inside the database
  • In the browser only
  • After the response is sent

Answer: Between the web server and your views. Middleware is a layer between the server and your views.

In the onion model, what does the FIRST middleware in MIDDLEWARE do?

  • Runs only on errors
  • Sees the request first and the response last
  • Never runs
  • Sees the response first

Answer: Sees the request first and the response last. The first middleware is the outermost layer of the onion.

Which method is called once at startup and stores get_response?

  • __call__
  • process_view
  • __init__(self, get_response)
  • handle

Answer: __init__(self, get_response). __init__(self, get_response) runs once and stores the next layer.

Which method runs for every request?

  • __init__
  • __call__(self, request)
  • process_exception
  • ready

Answer: __call__(self, request). __call__(self, request) is invoked per request.

What must __call__ always return?

  • None
  • The request
  • The response
  • A status code

Answer: The response. It must return the response or Django raises an error.

Which hook runs just before Django calls the view?

  • process_exception
  • process_template_response
  • process_view
  • process_request

Answer: process_view. process_view runs just before the view is called.

When does process_exception run?

  • On every request
  • At server startup
  • After every response
  • Only when the view raises an unhandled exception

Answer: Only when the view raises an unhandled exception. process_exception runs only on an unhandled view exception.

Why must SessionMiddleware come before AuthenticationMiddleware?

  • Alphabetical order
  • Auth reads the session, which must be attached first
  • Sessions need auth
  • It does not matter

Answer: Auth reads the session, which must be attached first. Authentication reads request.session, so the session must be set first.

Where do you activate a custom middleware?

  • By adding its dotted path to the MIDDLEWARE list
  • In urls.py
  • In the model
  • It is automatic

Answer: By adding its dotted path to the MIDDLEWARE list. Add the class's dotted path to MIDDLEWARE in settings.py.

What does the response phase order look like across the stack?

  • Top to bottom
  • Random
  • Same as request phase
  • Bottom to top (reverse of the request phase)

Answer: Bottom to top (reverse of the request phase). Responses travel outward, bottom-to-top, the reverse of requests.

Continue this course

Frequently asked questions

What is middleware in Django?

Middleware is a layer of code that sits between the web server and your views. Every request passes through each middleware on the way in, and every response passes back through them on the way out. This makes middleware the ideal place for cross-cutting concerns — things like authentication, logging, session handling, and security headers — that you want to apply to every request without repeating code in each view.

What is the onion model of middleware?

Django middleware is often described as an onion because requests travel inward through each layer to reach the view, and responses travel outward through the same layers in reverse order. The first middleware in the MIDDLEWARE list is the outermost layer: it sees the request first and the response last. The order you list middleware in therefore matters, because each layer wraps the ones below it.

How do I write custom middleware?

The modern style is a class with two methods. __init__(self, get_response) is called once when the server starts and stores the get_response callable. __call__(self, request) is called for every request: you run code before calling response = get_response(request), then run code after, and finally return the response. You add the dotted path to your class in the MIDDLEWARE list in settings.py.

What is the difference between process_view and process_exception?

These are optional hook methods Django calls on your middleware at specific points. process_view runs just before Django calls the view, giving you the view function and its arguments. process_exception runs only when a view raises an unhandled exception, letting you inspect the error and optionally return a custom response. Both are extra entry points beyond the main __call__ method.