Своя middleware и порядок

Пишем собственную middleware и разбираемся, почему её порядок в настройках так важен.

Middleware — это слой-обёртка вокруг view: код, который видит каждый запрос на входе и каждый ответ на выходе, образуя «луковицу» из вложенных слоёв вокруг обработчика.

Зачем это нужно на практике

Есть задачи, общие для всех страниц сайта: замерить время ответа, добавить заголовок безопасности, залогировать запрос, проверить блокировку пользователя, выставить язык интерфейса. Их объединяет то, что они не относятся к какой-то одной странице — они касаются всего трафика. Решать это в каждой view — копипаста и гарантированная дыра: про одну из ста view забудут, и там не будет ни лога, ни проверки. Middleware позволяет написать такую сквозную логику один раз и применить её централизованно ко всему, что проходит через приложение, — никого нельзя «пропустить мимо».

Это не экзотика: половина того, что вы считаете «встроенным поведением Django», — это middleware. Сессии (request.session), аутентификация (request.user), CSRF-защита форм, обработка GZip, добавление слеша в конец URL — всё это слои в списке MIDDLEWARE. Когда вы пишете свою middleware, вы встаёте в один ряд с этими механизмами и получаете тот же доступ к каждому запросу и ответу.

Как пишется middleware

Современный стиль — middleware как вызываемый объект. Это фабрика: при старте сервера Django вызывает её с аргументом get_response (следующий слой или сам view) и ждёт обратно функцию, которая будет вызвана на каждый запрос.

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response   # вызывается ОДИН раз при старте

    def __call__(self, request):
        # --- код ДО view (обработка запроса) ---
        response = self.get_response(request)   # передать управление дальше
        # --- код ПОСЛЕ view (обработка ответа) ---
        return response                          # вернуть ответ наверх

Запомните ритм: всё до get_response — это фаза запроса (выполняется на пути к view), всё после — фаза ответа (на пути обратно к клиенту). Вызов self.get_response(request) — это «пропустить дальше по цепочке»: управление уходит в следующую middleware, а в конце концов — в саму view, и возвращается уже с готовым response. Метод __init__ с аргументом get_response вызывается всего раз — когда сервер поднимается; именно поэтому ссылку на get_response сохраняют в self, чтобы потом дёргать на каждом запросе. Чтобы middleware вообще заработала, её путь добавляют строкой в список MIDDLEWARE в настройках проекта — без этой записи класс просто лежит в файле и никогда не вызывается:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "myapp.middleware.SimpleMiddleware",   # ваша
]

Порядок выполнения

Это самое важное и самое неочевидное. Список MIDDLEWARE работает как луковица: на входящем запросе слои проходят сверху вниз, а на исходящем ответе — снизу вверх, в обратном порядке.

Представьте три middleware A, B, C в этом порядке и view в центре. Маршрут запроса будет таким:

запрос  -> A (до) -> B (до) -> C (до) -> VIEW
ответ   <- A (после) <- B (после) <- C (после) <-

Отсюда практические следствия. Чем выше middleware в списке, тем «снаружи» она находится: первой видит запрос и последней трогает ответ. Поэтому порядок встроенных middleware не случаен: SessionMiddleware стоит выше AuthenticationMiddleware, ведь аутентификация читает сессию — сессия должна быть распакована раньше. Поменяете местами — request.user сломается. Если ваша middleware зависит от того, что сделала встроенная (например, читает request.user), ставьте её ниже неё.

Обработка запроса и ответа

Внутри __call__ вам доступны оба конца запроса-ответа. До вызова get_response можно читать и дополнять request — например, разобрать заголовок и положить в запрос новое поле, которое потом увидит view. После вызова у вас в руках готовый response, и его можно менять: навешивать заголовки, подправлять статус, логировать результат:

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

    def __call__(self, request):
        request.trace_id = request.headers.get("X-Trace-Id", "-")  # фаза запроса
        response = self.get_response(request)
        response["X-Trace-Id"] = request.trace_id                   # фаза ответа
        return response

Важно: можно «закоротить» цепочку. Если в фазе запроса вернуть HttpResponse, не вызывая get_response, — view и все нижние слои просто не выполнятся, ответ сразу пойдёт обратно наверх. Это легальный и полезный приём. Так делают, например, middleware технического обслуживания: при включённом режиме обслуживания она сразу отдаёт страницу «идут работы», не пуская запрос к базе и логике. Так же работают ограничители: если по IP исчерпан лимит запросов, middleware возвращает 429, не нагружая view. Главное — понимать, что закорачивание пропускает не только саму view, но и всю обработку запроса в слоях ниже текущего.

Реальные примеры

Замер времени ответа. Засекаем до, считаем после, кладём в заголовок и лог:

import time, logging
logger = logging.getLogger("timing")

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

    def __call__(self, request):
        start = time.monotonic()
        response = self.get_response(request)
        ms = (time.monotonic() - start) * 1000
        response["X-Response-Time-ms"] = f"{ms:.1f}"
        logger.info("%s %s -> %s за %.1f мс",
                    request.method, request.path, response.status_code, ms)
        return response

Логирование запросов. Тот же скелет, но пишем метод, путь, пользователя и статус каждого ответа — удобно для аудита и разбора инцидентов. Поскольку middleware видит весь трафик, это идеальное место для сквозного логирования, а не разбросанные по view вызовы.

Замер времени хорошо показывает, зачем нужна именно «луковица»: чтобы измерить полное время, TimingMiddleware должна стоять как можно выше — тогда её таймер охватит работу всех нижних слоёв и самого view.

Как это работает под капотом

При старте Django читает MIDDLEWARE и собирает цепочку снизу вверх: берёт реальный обработчик view, оборачивает его в последнюю middleware (передавая её view как get_response), результат оборачивает в предпоследнюю и так до самой верхней. Получается матрёшка функций, где get_response каждого слоя — это весь нижележащий стек. Конструкторы (__init__) вызываются один раз при сборке, а __call__ — на каждый запрос. Поэтому в __init__ уместна разовая подготовка, а вся работа с конкретным запросом — в __call__.

Частые ошибки

  • Забыли вернуть response из __call__ — клиент получит пустой/битый ответ или ошибку.
  • Неверный порядок в списке — middleware читает request.user или сессию, стоя выше того, кто их готовит; данные ещё не заполнены.
  • Тяжёлая работа в __init__ по ошибке на каждый запрос — на самом деле он зовётся раз, а вот код «на каждый запрос» нужно писать в __call__.
  • Состояние в атрибутах объекта — экземпляр middleware один на весь процесс и общий для всех запросов; храните данные в request, не в self.

Итоги

  • Middleware — сквозной слой вокруг view: код до get_response работает с запросом, после — с ответом.
  • Порядок в MIDDLEWARE критичен: вход — сверху вниз, выход — снизу вверх; верхние слои «снаружи».
  • Если зависите от сессии/пользователя — ставьте middleware ниже соответствующей встроенной.
  • Можно «закоротить» цепочку, вернув ответ до get_response (режим обслуживания, ранний отказ).
  • __init__ вызывается раз при старте, __call__ — на каждый запрос; состояние держите в request, а не в self.
Проверьте себя
1. В каком порядке middleware обрабатывают входящий запрос и исходящий ответ?
AИ запрос, и ответ — сверху вниз по списку MIDDLEWARE
BЗапрос — сверху вниз, ответ — снизу вверх (в обратном порядке)
CИ запрос, и ответ — снизу вверх
DПорядок случайный и не гарантируется
2. Куда поместить код, который должен выполняться ПОСЛЕ работы view (например, добавить заголовок к ответу)?
AВ __init__ middleware
BДо вызова self.get_response(request) в __call__
CПосле вызова self.get_response(request), перед return response
DВ отдельный метод process_request
3. Почему SessionMiddleware в списке стоит выше AuthenticationMiddleware?
AПросто по алфавиту
BЧтобы аутентификация выполнялась быстрее
CАутентификация читает данные сессии, поэтому сессия должна быть распакована раньше (быть «снаружи»)
DПорядок этих двух middleware не имеет значения