Пошук уроків, статей та іншого контенту
Приховаєте внутрішні деталі реалізації, налаштуєте винятки та повертатимете клієнту безпечні повідомлення про помилки.
Помилка на сервері може містити внутрішню інформацію:
назви таблиць і колонок бази даних;
SQL-запити;
шляхи до файлів;
стек викликів;
змінні середовища;
службові ідентифікатори та конфігурацію.
Якщо повернути таку інформацію клієнту, зловмисник зможе краще дослідити застосунок. Клієнту потрібне зрозуміле повідомлення, а розробнику — деталі в логах.
У NestJS для цього використовують:
вбудовані класи винятків;
власні винятки для безпечних повідомлень;
фільтри винятків;
логування внутрішніх помилок без передавання їх клієнту.
NestJS має готові класи, які відповідають HTTP-статусам:
import {
BadRequestException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
throw new BadRequestException('Некоректні дані запиту');
throw new NotFoundException('Користувача не знайдено');
throw new UnauthorizedException('Потрібна авторизація');NestJS перетворить таке виняття на HTTP-відповідь:
{
"statusCode": 404,
"message": "Користувача не знайдено",
"error": "Not Found"
}Повідомлення в таких винятках має бути безпечним. Не варто передавати клієнту текст помилки з бази даних:
// Небезпечно: внутрішні деталі можуть потрапити у відповідь
throw new BadRequestException(databaseError.message);Краще записати деталі в лог, а клієнту повернути загальне повідомлення:
// Безпечніше
logger.error(databaseError);
throw new BadRequestException('Не вдалося обробити запит');Щоб явно позначати повідомлення, які дозволено показувати клієнту, можна створити власний клас:
import { HttpException, HttpStatus } from '@nestjs/common';
export class SafeHttpException extends HttpException {
constructor(
message: string,
status: HttpStatus,
) {
super({ message }, status);
}
}Тепер у прикладному коді можна використовувати лише публічне повідомлення:
throw new SafeHttpException(
'Користувача не знайдено',
HttpStatus.NOT_FOUND,
);Це повідомлення не повинно містити:
тексту виняття з бази даних;
стеку викликів;
значень токенів;
SQL-запитів;
внутрішніх ідентифікаторів;
службових шляхів до файлів.
Фільтр винятків централізовано визначає, що повернути клієнту для різних типів помилок.
Фільтр нижче:
повертає безпечне повідомлення для SafeHttpException;
замінює інші HTTP-помилки на загальні повідомлення;
приховує деталі невідомих помилок;
записує внутрішні помилки в лог;
додає час і шлях запиту до відповіді.
// safe-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { SafeHttpException } from './safe-http.exception';
type ErrorResponse = {
statusCode: number;
message: string;
timestamp: string;
path: string;
};
@Catch()
export class SafeExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(SafeExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
const request = context.getRequest<Request>();
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Внутрішня помилка сервера';
if (exception instanceof SafeHttpException) {
statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (
typeof exceptionResponse === 'object' &&
exceptionResponse !== null &&
'message' in exceptionResponse &&
typeof exceptionResponse.message === 'string'
) {
message = exceptionResponse.message;
}
} else if (exception instanceof HttpException) {
statusCode = exception.getStatus();
message = this.getSafeMessage(statusCode);
} else {
this.logUnknownException(exception);
}
const errorResponse: ErrorResponse = {
statusCode,
message,
timestamp: new Date().toISOString(),
path: request.url,
};
response.status(statusCode).json(errorResponse);
}
private getSafeMessage(statusCode: number): string {
switch (statusCode) {
case HttpStatus.BAD_REQUEST:
return 'Некоректний запит';
case HttpStatus.UNAUTHORIZED:
return 'Потрібна авторизація';
case HttpStatus.FORBIDDEN:
return 'Доступ заборонено';
case HttpStatus.NOT_FOUND:
return 'Ресурс не знайдено';
case HttpStatus.CONFLICT:
return 'Операцію неможливо виконати';
case HttpStatus.TOO_MANY_REQUESTS:
return 'Забагато запитів';
default:
return statusCode >= 500
? 'Внутрішня помилка сервера'
: 'Не вдалося обробити запит';
}
}
private logUnknownException(exception: unknown): void {
if (exception instanceof Error) {
this.logger.error(exception.message, exception.stack);
return;
}
this.logger.error('Невідома помилка');
}
}Фільтр перевіряє SafeHttpException перед загальним HttpException, тому безпечне повідомлення з власного виняття зберігається. Для інших HTTP-винятків використовуються заздалегідь визначені повідомлення.
Глобальний фільтр обробляє помилки з усіх контролерів:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SafeExceptionFilter } from './safe-exception.filter';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new SafeExceptionFilter());
await app.listen(3000);
}
bootstrap();Після цього навіть помилка, яку випадково не обробили в сервісі, не поверне клієнту стек викликів або текст внутрішнього виняття.
// users.controller.ts
import {
Controller,
Get,
HttpStatus,
Param,
} from '@nestjs/common';
import { SafeHttpException } from './safe-http.exception';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) {
throw new SafeHttpException(
'Ідентифікатор користувача має бути додатним числом',
HttpStatus.BAD_REQUEST,
);
}
if (userId === 404) {
throw new SafeHttpException(
'Користувача не знайдено',
HttpStatus.NOT_FOUND,
);
}
if (userId === 500) {
// Імітація непередбаченої внутрішньої помилки
throw new Error('Помилка з’єднання з внутрішнім сховищем');
}
return {
id: userId,
name: 'Олена',
};
}
}Для запиту GET /users/404 клієнт отримає:
{
"statusCode": 404,
"message": "Користувача не знайдено",
"timestamp": "2026-09-01T10:00:00.000Z",
"path": "/users/404"
}Для запиту GET /users/500 клієнт отримає лише безпечну відповідь:
{
"statusCode": 500,
"message": "Внутрішня помилка сервера",
"timestamp": "2026-09-01T10:00:00.000Z",
"path": "/users/500"
}А в логах сервера залишиться технічна інформація про помилку.
Статус повинен описувати причину помилки:
400 Bad Request — запит має неправильний формат або некоректні значення;
401 Unauthorized — користувач не автентифікований;
403 Forbidden — користувач автентифікований, але не має доступу;
404 Not Found — ресурс не знайдено;
409 Conflict — операція конфліктує з поточним станом даних;
422 Unprocessable Entity — структура запиту правильна, але дані не можуть бути оброблені;
500 Internal Server Error — непередбачена помилка на сервері.
Не слід повертати 500, якщо клієнт надіслав некоректні дані. Така помилка виникла не через збій сервера, тому для неї підходить 400 або 422.
Логування та відповідь клієнту мають різне призначення:
лог містить деталі для діагностики;
HTTP-відповідь містить лише інформацію, необхідну клієнту.
Під час логування не записуйте секрети:
паролі;
access-токени;
refresh-токени;
ключі API;
повні платіжні дані;
приватні персональні дані.
Навіть якщо лог не повертається клієнту, він може зберігатися в системах моніторингу та бути доступним іншим працівникам.
Публічні деталі допустимі, якщо вони справді потрібні клієнту. Наприклад:
throw new SafeHttpException(
'Поле email має бути коректною електронною адресою',
HttpStatus.BAD_REQUEST,
);Натомість внутрішні деталі потрібно приховати:
try {
await this.repository.save(user);
} catch (error) {
this.logger.error(error);
throw new SafeHttpException(
'Не вдалося зберегти користувача',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}Клієнту не потрібно знати, чи сталася помилка через тайм-аут бази даних, відсутню таблицю або мережевий збій.
error.message клієнту// Небезпечно
catch (error) {
throw new BadRequestException(error.message);
}error.message може містити чутливі внутрішні дані. Замість цього запишіть помилку в лог і поверніть загальне повідомлення.
// Небезпечно
return response.status(500).json({
message: error.message,
stack: error.stack,
});Стек викликів розкриває структуру проєкту та спрощує пошук вразливостей. Він має залишатися тільки в серверних логах.
200 для помилки// Погано
return {
success: false,
error: 'Користувача не знайдено',
};Клієнт і проміжні системи орієнтуються на HTTP-статус. Для відсутнього ресурсу потрібно використати 404, а не 200.
this.logger.error('Помилка');Таке повідомлення складно досліджувати. Додавайте корисний контекст, але не секрети:
this.logger.error(
`Не вдалося завантажити користувача з id=${userId}`,
error.stack,
);Внутрішні деталі помилок не повинні потрапляти до клієнта.
Для очікуваних ситуацій використовуйте HTTP-виняття NestJS.
Публічні повідомлення формулюйте явно та без технічних деталей.
Непередбачені помилки повертайте як безпечну відповідь зі статусом 500.
Деталі й стек викликів записуйте в серверні логи.
Глобальний фільтр винятків допомагає однаково обробляти помилки в усьому застосунку.
Не записуйте паролі, токени та інші секрети навіть у логи.