awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Пишем middleware: сигнатуры, next() и три модели фреймворков

Суть Middleware — это функция с контрактом: прочитать запрос, при необходимости подействовать, затем вызвать next() или завершить ответ. Контракт различается в Express, Koa и Fastify, и самые частые продакшен-баги — это нарушение его одними и теми же тремя способами.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Middleware, добавляющий request ID, работает идеально. Его копия, которая ещё и вызывает асинхронный аудит-лог, начинает кидать Cannot set headers after they are sent под нагрузкой — но лишь иногда. Баг не в вызове аудита. Он в том, что асинхронная ошибка ускользает из middleware, дефолтный обработчик фреймворка отвечает, а потом отвечает и исходный путь кода. Два ответа, один сокет. Контракт middleware нарушен самым частым способом из существующих.

Сигнатура Express

В Express middleware — это (req, res, next) => {}. У него ровно три законных окончания:

  • Пропустить дальше — вызвать next(), чтобы передать управление следующему middleware.
  • Завершить — отправить ответ (res.json(...), res.end()) и не вызывать next(). Это коротит цепочку.
  • Упасть — вызвать next(err), чтобы перепрыгнуть к middleware обработки ошибок.

Два режима отказа — сделать оба (отправить ответ и вызвать next(), ведя к двойной отправке) или ни одного (нет ответа, нет next() — запрос висит до таймаута).

Error-middleware — другая форма

Express распознаёт middleware обработки ошибок по арности: оно должно объявлять четыре параметра, (err, req, res, next). Уберите неиспользуемый next — и Express сочтёт его обычным middleware и никогда не направит туда ошибки — тихий, бесящий баг. Error-middleware также должно быть зарегистрировано последним, после всех маршрутов, потому что цепочка достигает его лишь через next(err) из ранних слоёв.

Почему это работает

Почему Express 5 изменил обработку асинхронных ошибок? В Express 4 обработчик маршрута, вернувший отклонённый промис, не достигал error-middleware автоматически — необработанное отклонение ускользало, что и есть баг двойного ответа из Hook. Команды оборачивали каждый асинхронный обработчик в хелпер asyncHandler, чтобы поймать и перенаправить. Express 5 делает так, что обработчики маршрутов и middleware, возвращающие промис, автоматически вызывают next(err) при отклонении, так что обёртка больше не нужна. Сигнатура error-middleware с четырьмя аргументами не изменилась; автоматическим стало лишь перенаправление.

Koa: асинхронная луковица

Koa заменяет колбэк с тремя аргументами на async (ctx, next) => {} и настоящую луковичную модель. Код до await next() выполняется на пути внутрь; код после — на пути наружу, в обратном порядке. Это делает обёрточный тайминг тривиальным:

async (ctx, next) => {
  const start = Date.now();      // внутрь
  await next();                  // выполнить всё внутри
  ctx.set('X-Time', `${Date.now() - start}`); // наружу
}

Поскольку модель на промисах, throw где угодно естественно распространяется к вышестоящему try/catch-middleware — обработка ошибок это просто middleware, оборачивающий остальное в try/catch вокруг await next().

Fastify: хуки, а не один примитив, плюс инкапсуляция

Fastify разбивает единую идею middleware на именованные хуки жизненного цикла, срабатывающие в фиксированном порядке:

onRequest → preParsing → preValidation → preHandler → handler → onSend → onResponse

Это даёт отклонить рано на onRequest (до парсинга тела — самая дешёвая точка отказа) или выполнить логику на preHandler, когда уже есть валидированное тело. Бо́льшая разница — инкапсуляция: хуки (и декораторы), зарегистрированные внутри плагина, применяются лишь к маршрутам этого плагина, не глобально. Express-middleware, напротив, протекает вниз по всей цепочке от точки монтирования. Инкапсуляция — причина, по которой большие приложения Fastify остаются ограниченными по области, тогда как большие Express-приложения накапливают глобальный middleware.

МодельСигнатураОбёртываниеОбласть
Express(req, res, next)Вручную; error-mw нужно 4 аргументаГлобально от точки монтирования
Koaasync (ctx, next)Нативная луковица через await next()Глобальный стек
FastifyИменованные хукиХуки в фиксированных точках жизненного циклаИнкапсулировано на плагин
Викторина

Обработчик ошибок Express `(err, req, res) => {...}` никогда не получает ошибки, хотя ранний код вызывает next(err). Почему?

Викторина

Какова первопричина ошибки `Cannot set headers after they are sent` из middleware?

Викторина

Почему инкапсуляция Fastify важна по сравнению с middleware Express?

Вспомните перед уходом
  1. 01
    Каковы законные окончания middleware Express и два способа нарушить контракт?
  2. 02
    Как Express распознаёт error-middleware и что изменилось в Express 5?
  3. 03
    Сравните луковичную модель Koa с хуками и инкапсуляцией Fastify.
Итог

Middleware — это функция, связанная контрактом: прочитать запрос, при необходимости подействовать, затем сделать ровно одно из — передать управление через next(), завершить отправкой ответа или перенаправить ошибку через next(err). Его нарушение порождает два классических бага — ответить дважды («headers already sent») или не ответить никогда (зависание). Express определяет error-middleware по арности в четыре параметра и, начиная с версии 5, авто-перенаправляет отклонённые промисы, делая обёртку asyncHandler устаревшей. Koa выражает ту же идею как нативную асинхронную луковицу, где логика до и после await next() выполняется внутрь и наружу. Fastify разбивает middleware на фиксированные хуки жизненного цикла и добавляет инкапсуляцию, так что хуки на уровне плагина не протекают глобально, как middleware Express. С механикой оси запроса в руках следующий урок поворачивает к оси связывания: инверсии управления и тому, как зависимости реально добираются до класса.

Связанные уроки
встречается в185
Продолжить восхождение ↑Инверсия управления: как зависимости добираются до класса
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.