Конвейер 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 — это цепочка обработчиков, где порядок решает всё, а ответ всплывает в обратном направлении. Дальше — финальный раздел: безопасность и сборка приложения в прод.

Проверьте себя
1. Почему UseAuthentication должен идти перед UseAuthorization?
AЭто не важно
BСначала надо установить, кто пользователь (аутентификация), и только потом решать, что ему можно (авторизация)
CАвторизация быстрее
DОни взаимозаменяемы
2. Что произойдёт, если middleware не вызовет next()?
AНичего, всё работает как раньше
BКонвейер замкнётся (short-circuit): нижестоящие middleware и эндпоинт не выполнятся
CБудет ошибка компиляции
DЗапрос продублируется