Django Middleware: как работает и как написать свой
Что такое middleware в Django и как он обрабатывает запросы: порядок MIDDLEWARE, встроенные middleware, свой middleware, хуки, async и тесты.
Содержание · 12 разделов
- Как работает middleware в Django
- Встроенные middleware Django
- Как написать свой middleware
- Middleware-функция
- Middleware-класс
- Подключение в MIDDLEWARE
- Практические примеры middleware
- ID запроса для логов
- Режим обслуживания: прерывание цепочки
- Хуки middleware: process_view, process_exception, process_template_response
- process_view
- process_exception
- process_template_response
- Асинхронные middleware
- Старый стиль: MiddlewareMixin
- Как протестировать middleware
- Когда middleware не нужен
- Частые ошибки
- ‘NoneType’ object has no attribute ‘_resource_closers’
- ‘WSGIRequest’ object has no attribute ‘user’
- Module “core.middleware” does not define a ”…” attribute/class
- Middleware ломает скачивание файлов или потоковые ответы
- Middleware срабатывает не на всех запросах
- Частые вопросы
- Как применить middleware только к части URL?
- Как отключить проверку CSRF для одного view?
- Проходят ли запросы к статике через middleware?
- Как подключить middleware только в режиме разработки?
- Как получить IP-адрес клиента в middleware?
- Итог
Middleware в Django — это слой, через который проходит каждый запрос перед попаданием во view и каждый ответ на обратном пути к браузеру. Свой middleware — это вызываемый объект: класс, который получает get_response в __init__ и обрабатывает запрос в __call__. Подключается он строкой в списке MIDDLEWARE в settings.py.
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
В статье разберём, в каком порядке выполняются middleware, что делают встроенные middleware Django, как написать свой middleware для логирования, заголовков и режима обслуживания, как работают хуки process_view и process_exception, асинхронные middleware и тесты. Примеры проверены на Django 6.1 и 5.2 LTS.
Как работает middleware в Django
Список MIDDLEWARE в settings.py — это цепочка обёрток вокруг view. Запрос проходит её сверху вниз, ответ — снизу вверх. Каждый middleware получает get_response — ссылку на следующий слой (следующий middleware или саму view) — и сам решает, вызвать его или нет.
Проверим порядок на двух middleware, которые печатают сообщения:
# core/order.py
def make(name):
def factory(get_response):
def middleware(request):
print(f'{name}: запрос')
response = get_response(request)
print(f'{name}: ответ')
return response
return middleware
return factory
first = make('First')
second = make('Second')
# settings.py
MIDDLEWARE = ['core.order.first', 'core.order.second']
Вывод при запросе любой страницы:
First: запрос
Second: запрос
Second: ответ
First: ответ
Отсюда три следствия:
- Middleware выше по списку видит запрос раньше, а ответ — позже всех.
- Middleware может прервать цепочку: вернуть свой
HttpResponse, не вызываяget_response. Тогда нижние middleware и view не выполнятся, а ответ пройдёт только через те слои, что выше. - Порядок важен: если ваш middleware использует
request.user, он должен стоять нижеAuthenticationMiddleware, который этот атрибут создаёт.
Встроенные middleware Django
Проект, созданный командой startproject, уже содержит семь middleware:
# config/settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'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',
]
| Middleware | Что делает |
|---|---|
SecurityMiddleware | Заголовки безопасности и редирект на HTTPS по настройкам SECURE_* |
SessionMiddleware | Загружает и сохраняет сессию — создаёт request.session |
CommonMiddleware | Добавляет слеш в конце URL (APPEND_SLASH), блокирует User-Agent из DISALLOWED_USER_AGENTS, ставит Content-Length |
CsrfViewMiddleware | Проверяет CSRF-токен в POST-запросах |
AuthenticationMiddleware | Добавляет request.user на основе сессии |
MessageMiddleware | Включает флеш-сообщения (django.contrib.messages) |
XFrameOptionsMiddleware | Заголовок X-Frame-Options против clickjacking |
Порядок здесь не случайный: AuthenticationMiddleware берёт пользователя из сессии, поэтому стоит после SessionMiddleware. Админка Django при проверке конфигурации требует наличия SessionMiddleware, AuthenticationMiddleware и MessageMiddleware.
Другие встроенные middleware подключают по необходимости:
django.middleware.gzip.GZipMiddleware— сжатие ответов; ставится в начало списка, чтобы сжимать уже готовый ответ;django.middleware.locale.LocaleMiddleware— выбор языка по запросу; ставится послеSessionMiddlewareи передCommonMiddleware;django.middleware.cache.UpdateCacheMiddlewareиFetchFromCacheMiddleware— кеширование всего сайта: первый в начале списка, второй в конце;django.contrib.auth.middleware.LoginRequiredMiddleware(Django 5.1+) — требует вход на всех страницах, кроме помеченных декоратором@login_not_required;django.middleware.csp.ContentSecurityPolicyMiddleware(Django 6.0+) — заголовок Content Security Policy по настройкамSECURE_CSPиSECURE_CSP_REPORT_ONLY.
Как написать свой middleware
Middleware может быть функцией-фабрикой или классом: для простой обработки запроса и ответа подходят оба варианта.
Middleware-функция
Фабрика принимает get_response и возвращает функцию, обрабатывающую запрос. Пример добавляет заголовок с временем обработки:
# core/middleware.py
import time
def timing_middleware(get_response):
# Код здесь выполняется один раз — при запуске сервера
def middleware(request):
start = time.perf_counter()
response = get_response(request) # следующие middleware и view
duration_ms = (time.perf_counter() - start) * 1000
response['X-Response-Time'] = f'{duration_ms:.1f}ms'
return response
return middleware
Middleware-класс
Класс удобнее, когда нужны настройки, состояние или хуки. Логируем медленные запросы с порогом из settings.py:
# core/middleware.py
import logging
import time
from django.conf import settings
logger = logging.getLogger(__name__)
class SlowRequestLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.threshold = getattr(settings, 'SLOW_REQUEST_THRESHOLD', 1.0)
def __call__(self, request):
start = time.perf_counter()
response = self.get_response(request)
duration = time.perf_counter() - start
if duration > self.threshold:
logger.warning(
'Медленный запрос: %s %s — %.2f с',
request.method,
request.get_full_path(),
duration,
)
return response
Важно: __init__ вызывается один раз при запуске процесса, а __call__ — на каждый запрос. Не храните в атрибутах экземпляра данные конкретного запроса — один объект обслуживает все запросы, в том числе параллельные.
Подключение в MIDDLEWARE
Добавьте путь к классу или фабрике в settings.py:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ...стандартные middleware...
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'core.middleware.timing_middleware',
'core.middleware.SlowRequestLogMiddleware',
]
SLOW_REQUEST_THRESHOLD = 0.5 # секунды
Чтобы timing_middleware учитывал время работы остальных middleware, поставьте его в начало списка.
Практические примеры middleware
ID запроса для логов
Присваиваем каждому запросу идентификатор и возвращаем его в заголовке — удобно искать запрос в логах по жалобе пользователя:
import uuid
class RequestIDMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.request_id = request.headers.get('X-Request-ID') or uuid.uuid4().hex
response = self.get_response(request)
response['X-Request-ID'] = request.request_id
return response
Во view идентификатор доступен как request.request_id.
Режим обслуживания: прерывание цепочки
Middleware возвращает ответ 503, не пропуская запрос дальше. Если режим выключен, исключение MiddlewareNotUsed в __init__ убирает middleware из цепочки — на работающем сайте он не тратит ни такта:
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.http import HttpResponse
class MaintenanceModeMiddleware:
def __init__(self, get_response):
if not getattr(settings, 'MAINTENANCE_MODE', False):
raise MiddlewareNotUsed
self.get_response = get_response
def __call__(self, request):
if request.path.startswith('/admin/') or request.user.is_staff:
return self.get_response(request)
return HttpResponse('Сайт на обслуживании', status=503)
Этот middleware читает request.user, поэтому в MIDDLEWARE его ставят после AuthenticationMiddleware.
Хуки middleware: process_view, process_exception, process_template_response
Кроме __call__, класс middleware может определить специальные методы. Django вызывает их сам, если они есть.
| Хук | Когда вызывается | Порядок |
|---|---|---|
process_view(request, view_func, view_args, view_kwargs) | перед вызовом view, когда URL уже сопоставлен | сверху вниз |
process_exception(request, exception) | если view выбросила исключение | снизу вверх |
process_template_response(request, response) | если view вернула TemplateResponse | снизу вверх |
Каждый хук возвращает None (продолжить как обычно) или HttpResponse (использовать этот ответ).
process_view
Здесь уже известно, какая view обработает запрос и с какими аргументами:
class ViewNameMiddleware:
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('view=%s kwargs=%s', view_func.__name__, view_kwargs)
return None # продолжить обработку как обычно
Если process_view вернёт ответ, view не будет вызвана.
process_exception
Пример: для URL API отдаём JSON вместо HTML-страницы ошибки:
from django.http import JsonResponse
class ApiExceptionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_exception(self, request, exception):
if request.path.startswith('/api/'):
logger.exception('Ошибка API: %s', request.path)
return JsonResponse({'error': 'internal_error'}, status=500)
return None # пусть Django обработает ошибку стандартно
Хук ловит исключения из view и из отрисовки TemplateResponse. Ошибки, возникшие в самих middleware, в том числе в process_view, до него не доходят.
process_template_response
Позволяет поменять контекст или шаблон до отрисовки — но только для ответов TemplateResponse (их возвращают, например, class-based views). Обычный render() возвращает готовый HttpResponse, и хук для него не вызывается.
class SiteNameMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_template_response(self, request, response):
response.context_data['site_name'] = 'Django-Admin.ru'
return response
Хук обязан вернуть объект ответа. Для общих переменных во всех шаблонах обычно удобнее context processor.
Асинхронные middleware
По умолчанию middleware считается синхронным. Если проект работает под ASGI и содержит асинхронные view, синхронный middleware заставляет Django переключаться между режимами, что стоит производительности. Middleware, который умеет работать в обоих режимах, помечают декоратором sync_and_async_middleware:
from asgiref.sync import iscoroutinefunction
from django.utils.decorators import sync_and_async_middleware
@sync_and_async_middleware
def header_middleware(get_response):
if iscoroutinefunction(get_response):
async def middleware(request):
response = await get_response(request)
response['X-Powered-By'] = 'Django'
return response
else:
def middleware(request):
response = get_response(request)
response['X-Powered-By'] = 'Django'
return response
return middleware
Только асинхронный middleware-класс объявляет флаги и помечает себя как корутинную функцию:
from asgiref.sync import markcoroutinefunction
class AsyncHeaderMiddleware:
sync_capable = False
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
markcoroutinefunction(self)
async def __call__(self, request):
response = await self.get_response(request)
response['X-Async'] = '1'
return response
Для функций есть также декораторы sync_only_middleware и async_only_middleware из django.utils.decorators.
Старый стиль: MiddlewareMixin
В сторонних пакетах и старых проектах встречаются middleware с методами process_request и process_response, унаследованные от django.utils.deprecation.MiddlewareMixin:
from django.utils.deprecation import MiddlewareMixin
class LegacyHeaderMiddleware(MiddlewareMixin):
def process_request(self, request):
return None # или HttpResponse, чтобы прервать цепочку
def process_response(self, request, response):
response['X-Legacy'] = '1'
return response
Этот стиль по-прежнему работает — так написаны и некоторые встроенные middleware Django, например CommonMiddleware. Миксин сам реализует __call__ и поддерживает sync и async. Для нового кода проще писать обычный класс с __call__.
Как протестировать middleware
Middleware — обычный вызываемый объект, поэтому его можно проверить без запуска сервера: передайте фиктивную view и запрос из RequestFactory.
# core/tests.py
from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase
from .middleware import RequestIDMiddleware
class RequestIDMiddlewareTests(SimpleTestCase):
def setUp(self):
self.factory = RequestFactory()
self.middleware = RequestIDMiddleware(lambda request: HttpResponse('ok'))
def test_generates_id(self):
response = self.middleware(self.factory.get('/'))
self.assertEqual(len(response['X-Request-ID']), 32)
def test_keeps_incoming_id(self):
request = self.factory.get('/', headers={'X-Request-ID': 'abc'})
response = self.middleware(request)
self.assertEqual(response['X-Request-ID'], 'abc')
python manage.py test core
Для проверки всей цепочки используйте тестовый клиент self.client.get(...) — он проходит через все middleware из MIDDLEWARE. Значения настроек на время теста меняет декоратор @override_settings.
Когда middleware не нужен
Middleware выполняется на каждый запрос, поэтому подходит только для сквозной логики: безопасность, заголовки, логирование, локализация. Альтернативы:
- проверка доступа к отдельным страницам — декораторы
@login_required,@permission_requiredили миксины class-based views (подробнее — в статье об аутентификации в Django); - общие переменные в шаблонах — context processors в настройке
TEMPLATES; - реакция на сохранение моделей — сигналы или методы модели;
- тяжёлая работа (письма, отчёты) — фоновые задачи, например Celery.
Избегайте запросов к базе данных в middleware без кеширования: при каждом обращении к сайту они умножаются на число запросов.
Частые ошибки
‘NoneType’ object has no attribute ‘_resource_closers’
Middleware не вернул ответ — в __call__ забыт return response. Если сломанный middleware стоит не первым, ошибка выглядит иначе: слой выше получает None и падает на своей обработке ответа, например с AttributeError: 'NoneType' object has no attribute 'status_code'.
‘WSGIRequest’ object has no attribute ‘user’
Ваш middleware обращается к request.user, но стоит в списке выше AuthenticationMiddleware. Перенесите его ниже. То же с request.session и SessionMiddleware.
Module “core.middleware” does not define a ”…” attribute/class
Опечатка в пути в MIDDLEWARE или класс переименован. Путь должен совпадать с импортом: 'пакет.модуль.ИмяКласса'.
Middleware ломает скачивание файлов или потоковые ответы
У StreamingHttpResponse и FileResponse нет атрибута content. Если middleware читает или меняет тело ответа, проверяйте response.streaming и пропускайте такие ответы.
Middleware срабатывает не на всех запросах
Если middleware выше по списку вернул ответ (редирект CommonMiddleware, отказ CSRF, 503 режима обслуживания), нижние слои не выполняются. Ставьте логирование и замеры времени ближе к началу списка.
Частые вопросы
Как применить middleware только к части URL?
Middleware всегда стоит в общей цепочке, поэтому фильтр по адресу пишут внутри него: для «чужих» путей запрос сразу передают дальше.
class ApiOnlyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.path.startswith('/api/'):
return self.get_response(request)
response = self.get_response(request)
response['Cache-Control'] = 'no-store'
return response
Если логика нужна двум-трём view, проще написать декоратор.
Как отключить проверку CSRF для одного view?
Не убирайте CsrfViewMiddleware из MIDDLEWARE — это снимет защиту со всего сайта. Пометьте нужный view декоратором @csrf_exempt из django.views.decorators.csrf. Обычно так делают для вебхуков платёжных систем и других внешних сервисов, которые не могут прислать CSRF-токен.
Проходят ли запросы к статике через middleware?
Нет. В продакшне статику раздаёт Nginx, и Django этих запросов не видит. Под runserver файлы из STATIC_URL отдаёт обработчик django.contrib.staticfiles в обход цепочки MIDDLEWARE. Если статику раздаёт WhiteNoise, она отдаётся из его middleware, и слои ниже него не выполняются.
Как подключить middleware только в режиме разработки?
Добавьте его в список по условию в settings.py — так обычно подключают Django Debug Toolbar:
if DEBUG:
MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')
Второй вариант — выбросить MiddlewareNotUsed в __init__, как в примере с режимом обслуживания.
Как получить IP-адрес клиента в middleware?
Адрес лежит в request.META['REMOTE_ADDR']. За Nginx или балансировщиком там окажется адрес прокси, а реальный IP придёт в заголовке X-Forwarded-For. Доверяйте этому заголовку, только если его выставляет ваш прокси: клиент может прислать его сам и подменить адрес.
Итог
Middleware в Django — цепочка обёрток вокруг view: запрос идёт по MIDDLEWARE сверху вниз, ответ — снизу вверх. Свой middleware — это класс с __init__(get_response) и __call__(request). Хуки process_view, process_exception и process_template_response добавляют точки вмешательства, а sync_and_async_middleware делает его совместимым с ASGI.
Чтобы увидеть, где middleware находится в общей схеме обработки запроса, прочитайте руководство «Django для начинающих». Полный справочник — в официальной документации.
- #Django
- #Middleware
- #MIDDLEWARE
- #Архитектура
- #Async