Middleware своими руками в Django: что можно, а что не стоит
Собственный middleware в Django выглядит простым классом с __call__, но именно там ломают производительность и порядок обработки запроса. Разбираем, где он уместен, а где вы стреляете себе в ногу.

Middleware в Django выполняется на каждый запрос — до вьюхи и после неё. Один неудачный класс, который лезет в базу на каждом вызове, добавляет по 15–40 мс к каждому ответу сайта, включая отдачу статики и служебные эндпоинты. Проблема не в том, как написать middleware — это десять строк. Проблема в том, что многое из того, что туда пихают, там быть не должно: этому лучше место в декораторе вьюхи, в сигнале или в отдельном сервисе. Разбираем на Django 4.2 и 5.x, где собственный middleware оправдан, а где он превращается в скрытый налог на каждый запрос.
Как устроен современный middleware
С Django 1.10 действует стиль на основе вызываемого объекта. Старый стиль с методами process_request и process_response как отдельными точками входа убрали полностью — если вы видите его в туториале, туториал устарел. Актуальная форма — класс с __init__ и __call__:
# Django 4.2 / 5.x
class TimingMiddleware:
def __init__(self, get_response):
# Вызывается один раз при старте процесса, не на каждый запрос
self.get_response = get_response
def __call__(self, request):
# Код до вьюхи
import time
start = time.monotonic()
response = self.get_response(request)
# Код после вьюхи
elapsed_ms = (time.monotonic() - start) * 1000
response["X-Response-Time-ms"] = f"{elapsed_ms:.1f}"
return response
__init__ отрабатывает при инициализации приложения — сюда кладут то, что считается один раз. Всё, что внутри __call__ до self.get_response(request), крутится на каждом запросе до вьюхи. Всё после — после того, как вьюха вернула ответ. Порядок в MIDDLEWARE важен: на входе список проходится сверху вниз, на выходе — снизу вверх, как вложенные луковицы.
Дополнительные хуки
Помимо __call__ Django вызывает у middleware, если они определены, специальные методы. Их четыре, и они существуют именно в этих именах:
process_view(request, view_func, view_args, view_kwargs)— после того как URL сопоставлен с вьюхой, но до её вызова. Здесь вы уже знаете, какая вьюха отработает.process_exception(request, exception)— если вьюха бросила исключение. ВозвратHttpResponseотсюда подменяет обработку ошибки.process_template_response(request, response)— только для ответов с методомrender(то естьTemplateResponse), не для обычногоHttpResponse.
Метод process_request в новом стиле не вызывается — не путайте его с кодом в начале __call__.
Middleware видит все запросы без разбора: и к вашим страницам, и к /health, и к статике, если вы отдаёте её через приложение. Прежде чем добавить логику в middleware, спросите: она нужна на каждом запросе или на некоторых. Если на некоторых — это декоратор вьюхи, а не middleware.
Что стоит выносить в middleware
Оправданные кандидаты объединяет одно: логика действительно применима ко всему трафику и не зависит от того, какая вьюха отработает.
- Сквозной контекст запроса. Прокинуть
request_idв логи, привязать его кthreading.localилиcontextvars, чтобы каждая строка лога несла идентификатор. Это по определению общее для всех запросов. - Заголовки безопасности и служебные заголовки ответа. Если стандартных
SecurityMiddlewareи настроек вsettingsне хватает и нужен кастомный заголовок на весь сайт. - Ранняя фильтрация. Отсечь запросы по IP, по заблокированному
User-Agent, вернуть 403 до того, как отработает роутинг вьюх. Дешевле сделать это в начале__call__, чем внутри каждой вьюхи. - Лёгкое обогащение запроса. Положить в
requestданные, которые нужны большинству вьюх и которые не требуют похода в базу — например, распарсенный флаг фичи из заголовка.
Пример: request_id для логов
# Django 4.2 / 5.x, contextvars из стандартной библиотеки
import uuid
import contextvars
request_id_var = contextvars.ContextVar("request_id", default="-")
class RequestIDMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Берём из входящего заголовка или генерируем новый
rid = request.headers.get("X-Request-ID") or uuid.uuid4().hex
request_id_var.set(rid)
request.request_id = rid
response = self.get_response(request)
response["X-Request-ID"] = rid
return response
Дальше в форматтере логирования вы читаете request_id_var.get() и получаете сквозной идентификатор без передачи его руками через все слои.
Что в middleware класть не стоит
Основной вред от middleware — не ошибки, а незаметное замедление всего сайта. Логика, которая нужна на трёх вьюхах из ста, попадая в middleware, начинает выполняться на всех ста.
Запросы в базу на каждый запрос
Классика — middleware, который подгружает профиль пользователя, настройки или счётчик уведомлений и кладёт в request. Выглядит удобно, но теперь каждый запрос к сайту — включая favicon.ico и health-check — делает поход в базу. При 300 запросах в секунду это 300 лишних запросов к базе в секунду ради данных, которые нужны половине вьюх. Такое место — либо ленивое свойство (SimpleLazyObject, как это делает сам Django для request.user), либо декоратор на нужных вьюхах.
Бизнес-логика, привязанная к конкретным путям
Если внутри middleware вырос if request.path.startswith("/api/orders") с ветвлением на десяток случаев — это не middleware, это роутинг наизнанку. Django уже умеет сопоставлять пути с обработчиками через urls.py. Логику, зависящую от конкретного URL, разместите во вьюхе или её декораторе.
Тяжёлые вычисления и внешние вызовы
Поход в сторонний API, разбор большого JSON, обращение к Redis без таймаута — всё это в middleware блокирует ответ на каждом запросе. Внешний сервис затупил на 2 секунды — залёг весь сайт, а не один эндпоинт, который в этот сервис действительно ходит.
Middleware против альтернатив: куда что класть
| Задача | Куда положить | Почему |
|---|---|---|
| Логика на всём трафике (request_id, заголовки безопасности) | Middleware | Действительно нужна на каждом запросе |
| Проверка прав на группе вьюх | Декоратор или mixin вьюхи | Не грузит остальной трафик |
| Реакция на сохранение модели | Сигнал post_save или метод save | Привязано к данным, а не к HTTP-циклу |
| Тяжёлая работа после ответа | Очередь задач (Celery, django-q) | Не держит соединение с клиентом |
| Обработка исключений вьюхи | process_exception в middleware | Единая точка для всех вьюх |
Что ломается на практике
- Неправильный порядок в MIDDLEWARE. Если ваш middleware полагается на
request.user, он должен стоять нижеAuthenticationMiddlewareв списке. Иначеrequest.userещё не подставлен, и вы получитеAttributeError. - Возврат не-ответа.
__call__обязан вернуть объект ответа. Если в какой-то ветке вы забылиreturn, Django подниметValueErrorо том, что middleware не вернулHttpResponse. - Изменение уже отправленного ответа. Для стриминговых ответов (
StreamingHttpResponse) обращение кresponse.contentв постобработке сломает стриминг или прочитает генератор целиком в память. - Состояние на экземпляре. Не храните данные запроса в
self— экземпляр middleware один на процесс и разделяется между запросами. Данные конкретного запроса кладите вrequestили вcontextvars, иначе получите утечку данных между пользователями.
Async: не забудьте про совместимость
Django поддерживает асинхронные вьюхи, и middleware должен уметь работать в обоих режимах. Чисто синхронный middleware Django оборачивает адаптером, что добавляет накладные расходы при асинхронной обработке. Если пишете под async-стек, посмотрите механизм пометки middleware как sync- или async-совместимого через атрибуты класса — точные имена и порядок описаны в разделе документации об асинхронной поддержке middleware, сверьтесь с ним под вашу версию.
Что делать дальше
Перед тем как добавить свой middleware, замерьте цену: включите django-debug-toolbar в разработке и посмотрите, сколько запросов к базе и времени добавляет ваш класс на простой странице. В проде следите за перцентилями времени ответа по всему сайту, а не по отдельной вьюхе — рост p95 после деплоя нового middleware виден сразу. Если логика оказалась привязана к конкретным путям — вынесите её в декоратор вьюхи и удалите из списка. Полный перечень встроенных middleware и их порядок держите под рукой: часто нужный вам эффект уже реализован в стандартном классе, и свой писать не придётся.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Django documentation — Middleware, Django documentation — Asynchronous support
Частые вопросы
У меня Django 3.2, отличается ли синтаксис middleware?
Middleware не срабатывает — что проверить в первую очередь?
Можно ли в middleware прервать запрос и сразу вернуть ответ, не доходя до вьюхи?
Чем middleware отличается от декоратора вьюхи и когда что выбирать?
Безопасно ли хранить данные запроса в атрибуте self внутри middleware?
Как понять, что мой middleware замедляет сайт?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.