Раздел 8 · Frameworks и Libs

Fastify: декораторы и жизненный цикл

middle~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Fastify: декораторы и жизненный цикл

Плагины из урока 02 дали нам дерево областей. Теперь два вопроса: как расширять request и reply своими полями (декораторы) и где именно в обработке запроса можно вклиниться (хуки жизненного цикла). Вместе это инструмент, которым в Fastify пишут всю сквозную логику: аутентификацию, логирование, контекст запроса. И всё это уважает инкапсуляцию из урока 02: хук виден только своей области и потомкам.

1. Декораторы: расширяем инстанс, запрос и ответ

Декоратор это способ добавить поле или метод к одному из трёх объектов Fastify: инстансу приложения, объекту request или объекту reply.

// на инстанс: общий сервис или конфиг, живёт всё время работы
app.decorate('db', databaseClient);

// на каждый запрос: поле, которое заполняется по ходу обработки
app.decorateRequest('user', null);

// на каждый ответ: хелпер ответа
app.decorateReply('ok', function (payload: unknown) {
  return this.send({ ok: true, data: payload });
});
  • decorate(name, value) кладёт значение на инстанс. Это shared состояние: один объект на всё приложение (или область). Так регистрируют сервисы, мы делали это с db через fp в уроке 02.
  • decorateRequest(name, value) добавляет поле каждому request.
  • decorateReply(name, value) добавляет метод или поле каждому reply.

1.1 Ловушка: объекты в decorateRequest

Главная тонкость декораторов запроса. Если передать в decorateRequest объект, он станет общим для всех запросов: Fastify положит одну и ту же ссылку каждому request, и запросы начнут затирать данные друг друга.

// ОПАСНО: один объект session на все запросы сразу
app.decorateRequest('session', { userId: null });

// ПРАВИЛЬНО: примитив-плейсхолдер, наполняем в хуке
app.decorateRequest('user', null);

Для примитивов (null, число, строка) проблемы нет: их Fastify копирует. Для объектов нужно либо ставить null и наполнять поле в хуке (см. § 3), либо использовать фабрику через геттер:

// каждый запрос получит свой свежий объект
app.decorateRequest('session', {
  getter() {
    return { userId: null };
  },
});

Почему так, понятно из урока 06-node про модель памяти: объект это ссылка, и положить одну ссылку всем, значит дать им общий мутабельный стейт.

2. Полный жизненный цикл запроса

Каждый запрос в Fastify проходит фиксированную последовательность стадий, и в большинство из них можно вклиниться хуком. Вот конвейер по порядку:

onRequest          запрос принят, тела ещё нет
  -> preParsing    до парсинга тела (сырой поток)
  -> preValidation тело распарсено, до проверки схемой
  -> preHandler    тело провалидировано (тут обычно auth)
  -> handler       основная логика роута
  -> preSerialization payload до сериализации
  -> onSend        сериализованный ответ, последний шанс
  -> onResponse    ответ отправлен (только логи и метрики)

Плюс две ветки в стороне: onError (если где-то брошено исключение) и onTimeout (если запрос превысил лимит). И отдельный класс, application-хуки: onReady (приложение готово), onClose (останов), onRoute (зарегистрирован роут), onRegister (зарегистрирован плагин).

Прокрути запрос по конвейеру: выбери сценарий и жми «шаг». Посмотри, как успешный путь проходит все стадии, как preHandler с отказом короткозамыкает на onSend мимо handler, и как брошенная ошибка уводит в ветку onError.

Жизненный цикл запроса
  1. onRequest
  2. preParsing
  3. preValidation
  4. preHandler
  5. handler
  6. preSerialization
  7. onSend
  8. onResponse
  9. onError (ветка ошибки)
нажми «шаг»

Запрос ещё не вошёл в конвейер.

2.1 Где что делают

Стадии не взаимозаменяемы, у каждой своя роль:

  • onRequest: самое раннее. Тела ещё нет, поэтому тут дёшево отказать (ранний rate-limit, проверка IP), поставить reqId, начать таймер.
  • preHandler: тело уже провалидировано. Это каноническое место аутентификации: достать пользователя из токена и положить в request.user, при нехватке прав отклонить.
  • preSerialization: payload ещё объект. Удобно обернуть ответ в общий конверт { data, meta } на уровне области.
  • onSend: тело уже строка или буфер. Последний шанс поменять заголовки или подменить тело.
  • onResponse: ответ уже у клиента, менять нечего. Только метрики и лог длительности.

2.2 Прерывание и порядок

Хук может прервать обработку, отправив ответ через reply.send (или бросив ошибку). Тогда оставшиеся хуки до handler и сам handler пропускаются, как в сценарии «preHandler отклоняет» на виджете.

app.addHook('preHandler', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'unauthorized' });
    // handler не вызовется: ответ уже отправлен
  }
});

В async-хуке для прерывания достаточно return reply после send или просто throw. Несколько хуков одной стадии выполняются в порядке регистрации.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.