КурсыNestJSТема 10

NestJS / 38 минут

Исключения и filters

Преобразуйте исключения в единый безопасный HTTP-контракт.

В процессе Теория: впереди Практика: впереди Тест: впереди
10 / 14
После этой главы
  • Преобразуйте исключения в единый безопасный HTTP-контракт.
  • Применить тему в задаче «Исключения и filters» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Ошибочная ветвь HTTP

Исключение прерывает обычный путь выполнения. Nest имеет стандартную обработку HttpException и производных классов: BadRequestException, NotFoundException, ForbiddenException и других. Неожиданный внутренний сбой обычно превращается в 500. Exception filter позволяет согласовать внешний формат ошибок и сохранить различие между отказом клиента и поломкой сервера.

Контроллер или сервис может бросить исключение, но обязан понимать его смысл. Не найденная книга и недоступная БД — разные ситуации. Если любую ошибку перехватить и превратить в 404, клиент получит ложную информацию, а операторы потеряют сигнал сбоя. Отправляйте безопасный внешний ответ, сохраняя внутреннюю причину в защищённой диагностике.

TypeScript
@Get(':id')
async find(@Param('id') id: string) {
  const book = await this.books.find(id);
  if (!book) throw new NotFoundException('Книга не найдена');
  return book;
}

Тип и область перехвата

Catch задаёт типы исключений, которые обрабатывает фильтр. ExceptionFilter реализует catch с исключением и ArgumentsHost. Для HTTP switchToHttp предоставляет request и response. Пустой Catch перехватывает любые ошибки, поэтому требует аккуратного различения известных и неожиданных случаев. Не считайте произвольный объект исключением с getStatus без проверки.

TypeScript
@Catch(HttpException)
class RequestFilter implements ExceptionFilter {
  catch(error: HttpException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse<Response>();
    response.status(error.getStatus()).json({ error: 'request' });
  }
}

Response здесь импортируется как тип из express для Express-адаптера. Если проект должен поддерживать разные HTTP-адаптеры, используйте HttpAdapterHost вместо привязки к конкретным методам ответа. Выбор адаптера — техническая деталь инфраструктуры; он не должен разрастаться по всем доменным сервисам.

Безопасный контракт ошибок

Клиенту полезны стабильный код ошибки, понятное сообщение и при необходимости безопасные ошибки полей. Stack trace, SQL, абсолютные пути и credentials не нужны для исправления пользовательского ввода. Даже сообщение известного HttpException может содержать данные, которые не следует пересылать без проверки, если оно создано из текста стороннего сервиса.

TypeScript
type PublicError = {
  error: { code: 'NOT_FOUND' | 'INVALID_INPUT' | 'INTERNAL'; message: string };
};
const internal: PublicError = {
  error: { code: 'INTERNAL', message: 'Не удалось выполнить запрос' },
};

Такой контракт не обязан совпадать с форматом успеха. Не маскируйте ошибку кодом 200 ради одинакового JSON: кеши, мониторинг и клиенты ориентируются на HTTP-статус. Для связи обращения с логами можно возвращать безопасный идентификатор запроса, не содержащий токен сессии или секретную информацию.

Где зарегистрировать фильтр

UseFilters назначает фильтр методу или контроллеру. Глобальный APP_FILTER удобен для единой границы приложения с DI. Передача new SafeFilter в useGlobalFilters подходит для простого независимого фильтра. Если фильтру нужен logger или HTTP adapter, контейнерная регистрация делает зависимость явной и проверяемой.

TypeScript
@Module({
  providers: [{ provide: APP_FILTER, useClass: GlobalErrorFilter }],
})
class AppModule {}

GlobalErrorFilter в примере — реализованный в приложении класс, APP_FILTER импортируется из @nestjs/core. Фильтр конкретного маршрута может обработать ошибку раньше глобального. Не рассчитывайте на последовательное «дополнение» одного ответа всеми filters: определите единственного владельца формата и проверяйте область назначения.

Ресурсы и повторный ответ

Фильтр должен завершить ответ один раз. После отправки заголовков повторный json приводит к дополнительной ошибке. Потоковые ответы требуют отдельного подхода: часть данных могла уйти клиенту до сбоя. Не пытайтесь в таком случае заменить уже начатый файл обычным JSON-контрактом без проверки состояния ответа.

Освобождение ресурса принадлежит тому коду, который им владеет. Используйте finally для подключения, блокировки или временного объекта, а не надейтесь, что filter догадается обо всех открытых ресурсах. Асинхронную фоновую задачу, не включённую в цепочку запроса, стандартный HTTP filter также не обязан перехватить: у фоновых процессов должна быть собственная обработка ошибок.

Практика безопасного отказа

Создайте GET /fail, бросающий BadRequestException, и фильтр для HttpException. Он сохраняет статус исключения и отвечает только объектом {error: "request"}. solve возвращает реальные status и body из HTTP-проверки. Частное сообщение исходного исключения не должно попасть в ответ. Контекст приложения закрывается в finally.

После решения добавьте в локальном проекте отдельный случай неожиданного Error и убедитесь, что он даёт 500 с безопасным сообщением. Проверьте отсутствие stack и внутренних полей. Это проверка наблюдаемого поведения API; точное количество вызовов вспомогательного метода логирования само по себе не доказывает безопасность внешнего ответа.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0