skip to content

Hook Protocol & Ordering

A middleware is a factory taking get_response and returning a callable, plus optional process_view, process_exception and process_template_response hooks. Interviewers probe short-circuits.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

How do you write a custom Django middleware, and which part of it runs once versus on every request?

level: juniorimportance: must knowfreq 62%

answer

  1. a factory, not a plain function
  2. the next layer is passed in
  3. constructor versus call
  4. a dotted path in a list

basics

~10 s

Write a factory that receives get_response: a class whose init(self, get_response) runs once at startup and whose call(self, request) runs per request, calling get_response and returning the response. Register its dotted path in MIDDLEWARE.

solid answer

~40 s

A Django middleware is a **factory**: a class (or function) that Django calls once, when the handler loads `MIDDLEWARE` at startup, passing `get_response`, the next layer of the chain. `__init__` may take only `get_response` and is the place for one-time setup; raising `django.core.exceptions.MiddlewareNotUsed` there removes the middleware from the chain. `__call__(request)` runs on every request: code before `self.get_response(request)` sees the request on the way in, code after it sees the response on the way out, and it must return a response. A request-timing middleware reads `time.perf_counter()` before the call, sets a `Server-Timing` header on the response after it, and returns it. You activate it by adding its dotted path, such as `"core.middleware.TimingMiddleware"`, to the `MIDDLEWARE` list, and its position there decides what it wraps.

code

python · 20 lines
python
import time

from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed


class TimingMiddleware:
    def __init__(self, get_response):
        # Runs once, when Django builds the middleware chain.
        if not getattr(settings, "REQUEST_TIMING_ENABLED", True):
            raise MiddlewareNotUsed("request timing disabled")
        self.get_response = get_response

    def __call__(self, request):
        # Runs on every request.
        start = time.perf_counter()
        response = self.get_response(request)
        elapsed_ms = (time.perf_counter() - start) * 1000
        response["Server-Timing"] = f"app;dur={elapsed_ms:.1f}"
        return response

go deeper

for a junior

Recall the two methods, init with get_response and call with request, that the response must be returned, and that the dotted path goes into MIDDLEWARE.

for a middle

Explain that Django instantiates each entry once when it builds the chain, why that makes self unsuitable for request data, and what MiddlewareNotUsed does at startup.

for a senior

Show judgement about what is safe to compute once, where a timing layer should sit to measure what you care about, and how a missing return or a shared attribute fails under load.

for a principal

Decide which cross-cutting concerns deserve a project middleware at all, since every layer runs on every request, and which belong in views or decorators instead.

## The factory protocol In Django a **middleware factory** is a callable that takes one argument, `get_response`, and returns a **middleware**: a callable that takes a request and returns a response, exactly like a view. `get_response` is whatever comes next: the next middleware in the list, or, for the last one, a handler wrapper that resolves the URL and calls the view. A middleware does not need to know which. Most middleware is written as a class, because the class itself is the factory and its instances are the middleware: - the class is called with `get_response`, so `__init__(self, get_response)` runs; - the resulting instance is called with each request, so `__call__(self, request)` runs. A function-based factory works the same way: an outer function receives `get_response`, does its setup and returns an inner function that handles each request. ## Once versus per request | Part | When it runs | What belongs there | |---|---|---| | `__init__(self, get_response)` | once, when Django builds the chain at startup | store `get_response`, read settings, build expensive helpers, or raise `MiddlewareNotUsed` | | code before `self.get_response(request)` | on every request, on the way in | inspect or annotate the request, or return a response early | | code after it | on every request, on the way out | inspect or modify the response | The once-only `__init__` is the part people get wrong. Django's handler calls `load_middleware()` when it is created, which instantiates every entry in `MIDDLEWARE` a single time; after that, the same instance serves every request the process handles. Anything specific to one request therefore belongs in local variables inside `__call__` or on the `request` object, never on `self`. ## A request-timing middleware The classic first middleware measures how long the rest of the stack took: 1. `__init__` stores `get_response` (and, optionally, decides whether timing is enabled at all). 2. `__call__` records `time.perf_counter()` in a local variable. 3. It calls `self.get_response(request)`, which runs every layer below it and the view. 4. It computes the elapsed time and writes it into a response header, such as `Server-Timing: app;dur=12.3`. 5. It returns the response, so the layers above it, and finally the server, receive it. Forgetting step 5 is a common bug: the layer above gets `None` instead of a response and the request ends in an error. ## Registering it A middleware does nothing until its **dotted Python path** is added to the `MIDDLEWARE` setting. Django's own default for that setting is an empty list; `startproject` writes a list of seven built-ins into the generated `settings.py`. The order is meaningful: the first entry is the outermost layer, so it sees the request first and the response last. A timing middleware placed near the top measures almost the whole stack; one placed at the bottom measures little more than the view. The older `MIDDLEWARE_CLASSES` setting, with its separate `process_request`/`process_response` protocol, was removed in Django 2.0. Django's built-in middleware still defines those two methods through `django.utils.deprecation.MiddlewareMixin`, whose `__call__` invokes them around `get_response`, but new middleware can simply implement `__call__`. ## Switching it off at startup - Raising `MiddlewareNotUsed` from `__init__` tells Django to leave the middleware out of the chain entirely; when `DEBUG` is true, Django logs a debug message to the `django.request` logger. - Because the decision happens in `__init__`, it is made once per process, not per request. A per-request opt-out belongs in `__call__`, as an early branch that just calls `get_response`. - A factory that returns `None` instead of a middleware is a configuration error: Django raises `ImproperlyConfigured` naming the factory. ## Rules that keep it correct - `__init__` must accept exactly `get_response`; Django passes nothing else, so configuration comes from settings or class attributes. - Always return a response from `__call__`. - Keep per-request data out of `self`. - Reading `request.POST` before the view runs prevents the view from changing upload handlers, so avoid it in general-purpose middleware. - Not every response has `content`: a `StreamingHttpResponse` exposes `streaming_content` instead. Setting a header works on both.

  • Why can a Django middleware's __init__ take only get_response?
    Django instantiates each `MIDDLEWARE` entry itself and passes exactly one argument, the next layer. Configuration therefore comes from settings or class attributes read inside `__init__`. Because `__init__` runs once per process, whatever it reads is fixed until the process restarts, which is right for configuration and wrong for anything per request.
  • What happens if a middleware's __call__ forgets to return the response?
    The layer above receives `None` where it expects an `HttpResponse`. As soon as that layer or the handler touches the response, for example to set a header, it fails, and the request ends in an error, typically a 500. Returning the response on every path, including early branches, avoids it.
  • When would you raise MiddlewareNotUsed instead of branching in __call__?
    When the decision is fixed for the life of the process, such as a feature disabled by a setting in this environment. Raising it in `__init__` removes the layer from the chain, so disabled middleware costs nothing per request. A decision that depends on the request, such as skipping health-check paths, belongs in `__call__`.

saying these in an interview costs you the question

  • __init__ runs on every request, so it can hold per-request data.
  • A middleware must subclass a Django base class to be accepted.
  • Custom middleware is registered in MIDDLEWARE_CLASSES.
  • Raising MiddlewareNotUsed skips the middleware for the current request only.
  • Code after get_response runs before the view is called.
open as a page

In Django, which middleware see the response when one returns early from __call__ versus from process_view?

level: middleimportance: should knowfreq 42%

basics

~20 s

An early return from call is seen only by that middleware and those listed above it; lower layers and the view never run. A response from process_view skips the view, yet every middleware's call after-phase still sees it.

open as a page

In Django middleware, when do process_view, process_exception and process_template_response run, and in what order across the MIDDLEWARE list?

level: middleimportance: should knowfreq 45%

basics

~20 s

process_view runs top-down after URL resolution, just before the view; process_exception runs bottom-up only when the view raises; process_template_response runs bottom-up when the response has render(). A process_view or process_exception response stops the remaining hooks of that kind.

open as a page

A Django timing middleware stores its start time on self in __call__; why are its durations wrong under concurrent load, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Django creates one middleware instance per process at startup and every request shares it, so a concurrent request overwrites self.start and durations come out too short. Keep per-request state in a local variable in call or on the request object.

open as a page