Своя 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.