Django-Admin.ru

Django Middleware: как работает и как написать свой

Что такое middleware в Django и как он обрабатывает запросы: порядок MIDDLEWARE, встроенные middleware, свой middleware, хуки, async и тесты.

Редакция Django-Admin.ru
· обновлено · 11 мин чтения

Содержание · 12 разделов
  1. Как работает middleware в Django
  2. Встроенные middleware Django
  3. Как написать свой middleware
  4. Middleware-функция
  5. Middleware-класс
  6. Подключение в MIDDLEWARE
  7. Практические примеры middleware
  8. ID запроса для логов
  9. Режим обслуживания: прерывание цепочки
  10. Хуки middleware: process_view, process_exception, process_template_response
  11. process_view
  12. process_exception
  13. process_template_response
  14. Асинхронные middleware
  15. Старый стиль: MiddlewareMixin
  16. Как протестировать middleware
  17. Когда middleware не нужен
  18. Частые ошибки
  19. ‘NoneType’ object has no attribute ‘_resource_closers’
  20. ‘WSGIRequest’ object has no attribute ‘user’
  21. Module “core.middleware” does not define a ”…” attribute/class
  22. Middleware ломает скачивание файлов или потоковые ответы
  23. Middleware срабатывает не на всех запросах
  24. Частые вопросы
  25. Как применить middleware только к части URL?
  26. Как отключить проверку CSRF для одного view?
  27. Проходят ли запросы к статике через middleware?
  28. Как подключить middleware только в режиме разработки?
  29. Как получить IP-адрес клиента в middleware?
  30. Итог

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: ответ

Отсюда три следствия:

  1. Middleware выше по списку видит запрос раньше, а ответ — позже всех.
  2. Middleware может прервать цепочку: вернуть свой HttpResponse, не вызывая get_response. Тогда нижние middleware и view не выполнятся, а ответ пройдёт только через те слои, что выше.
  3. Порядок важен: если ваш 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
↑ ↓ выборEnter открытьEsc закрыть