Конвейер middleware
Конвейер middleware: как каждый запрос проходит через цепочку обработчиков.
Суть: middleware — это компоненты, через которые последовательно проходит каждый HTTP-запрос. Каждый может что-то сделать с запросом, передать дальше (
next) и обработать ответ на обратном пути. Порядок их подключения определяет поведение приложения.
Между приходом запроса и вашим контроллером запрос проходит через конвейер: проверка HTTPS, аутентификация, авторизация, обработка ошибок. Каждый этап — это middleware. Понимание конвейера и его порядка — одна из важнейших тем ASP.NET Core.
Конвейер и его порядок
app.UseExceptionHandler("/error"); // 1. ловит ошибки ниже
app.UseHttpsRedirection(); // 2.
app.UseRouting(); // 3. выбор эндпоинта
app.UseCors(); // 4. после routing
app.UseAuthentication(); // 5. кто ты?
app.UseAuthorization(); // 6. что тебе можно?
app.MapControllers(); // 7. выполнение эндпоинта
Порядок не случаен. Обработка ошибок — первой, чтобы поймать всё ниже. Аутентификация — до авторизации (сначала «кто ты», потом «что можно»). И то и другое — после routing, ведь авторизация смотрит на атрибуты выбранного эндпоинта.
Запрос и ответ через конвейер
Запрос ->
[Exception] -> [HTTPS] -> [Routing] -> [Auth] -> [Authz] -> [Endpoint]
|
ответ <- [Exception] <- [HTTPS] <- [Routing] <- [Auth] <- [Authz] <-+
Каждый middleware видит запрос на пути вперёд
и ответ на пути назад (как стопка матрёшек).
Как работает под капотом
Технически каждый middleware — это функция, которая получает HttpContext и ссылку на next. Она может выполнить код до await next() (на пути запроса), вызвать следующий, и выполнить код после (на пути ответа). Конвейер — вложенные вызовы: первый middleware оборачивает второй, тот — третий и так до эндпоинта. Поэтому ответ «всплывает» обратно в обратном порядке. Если middleware не вызовет next() — он замкнёт конвейер (short-circuit), и нижестоящие не выполнятся (так, например, работает кэш или блокировка).
# Конвейер middleware: код до и после next, ответ всплывает обратно
def exception_mw(ctx, nxt):
try:
nxt(ctx)
except Exception as e:
ctx["status"] = 500; ctx["body"] = f"Ошибка: {e}"
def auth_mw(ctx, nxt):
print("auth: проверяю токен (путь вперёд)")
nxt(ctx)
print("auth: запрос обработан (путь назад)")
def endpoint(ctx, nxt):
print("endpoint: формирую ответ")
ctx["status"] = 200; ctx["body"] = "OK"
def build(mws):
def chain(i):
def run(ctx):
nxt = chain(i + 1) if i + 1 < len(mws) else (lambda c: None)
mws[i](ctx, nxt)
return run
return chain(0)
pipeline = build([exception_mw, auth_mw, endpoint])
ctx = {}
pipeline(ctx)
print(ctx)
Попробуй сам ▶ — обратите внимание на порядок вывода: «auth путь вперёд», затем «endpoint», затем «auth путь назад». Ответ действительно всплывает обратно, как в реальном конвейере.
Частые ошибки
- UseAuthorization до UseAuthentication. Авторизация не узнает пользователя — порядок строго: сначала Authentication.
- UseCors не на своём месте. CORS — после routing и до auth, иначе preflight-запросы ломаются.
- Забыть вызвать
next(). Случайный short-circuit — нижние middleware и эндпоинт не сработают.
Best practices
- Держите канонический порядок: Exception, HTTPS, Routing, CORS, Authentication, Authorization, Endpoints.
- Обработку ошибок ставьте первой, чтобы централизованно превращать исключения в корректные ответы.
- Выносите свою логику в кастомный middleware, когда она нужна для всех запросов (трассировка, заголовки).
Use, Run, Map и устройство звена
Конвейер собирают тремя видами вызовов. Use добавляет middleware, которое может выполнить код, вызвать next() и продолжить после него — самый частый случай. Run добавляет терминальное middleware, которое next не вызывает и замыкает конвейер. Map ответвляет конвейер по пути запроса (например отдельная ветка для /admin). Технически каждое звено — это функция (HttpContext, RequestDelegate) => Task: получает контекст и делегат «следующего», решает, что сделать до и после вызова следующего.
Из вложенности звеньев следует ключевое свойство: запрос идёт «вниз» по конвейеру до эндпоинта, а ответ «всплывает» обратно в обратном порядке. Middleware, добавленное первым, видит запрос раньше всех и ответ позже всех — оно как самая внешняя матрёшка. Поэтому обработчик ошибок ставят первым: он оборачивает весь конвейер и ловит исключения из любого нижестоящего звена.
Канонический порядок и почему он именно такой
Порядок middleware — не вкусовщина, а требование корректности. Сначала обработка исключений (чтобы ловить всё ниже), затем HTTPS-редирект, затем UseRouting (выбор эндпоинта), затем CORS, затем UseAuthentication и только потом UseAuthorization, и в конце выполнение эндпоинта. Аутентификация строго перед авторизацией: нельзя проверять права, не зная пользователя. CORS — после routing (ему нужны метаданные эндпоинта) и до auth (preflight-запросы OPTIONS не должны блокироваться авторизацией). Перестановка этих звеньев тихо ломает безопасность или CORS, причём ошибка может проявиться не сразу.
Когда сквозная логика нужна для всех запросов — трассировка, добавление заголовков, замер времени, корреляционные id — её оформляют как кастомное middleware и ставят в нужное место конвейера. Если же middleware решает не пускать запрос дальше (не вызывает next()), происходит short-circuit: нижестоящие звенья и эндпоинт не выполняются. Так реализуют кэш (отдать готовый ответ сразу), rate limiting (отклонить лишний запрос), раннюю проверку. Запускаемая врезка выше наглядно показала «путь вперёд и путь назад» — держа эту модель в голове, вы безошибочно расставите middleware в любом приложении.
Итог: конвейер middleware — это цепочка обработчиков, где порядок решает всё, а ответ всплывает в обратном направлении. Дальше — финальный раздел: безопасность и сборка приложения в прод.