Пошук уроків, статей та іншого контенту
Навчитеся отримувати інформацію про HTTP-запит, контролер і обробник у guards, interceptors та filters.
ExecutionContextExecutionContext — це об’єкт із інформацією про поточне виконання обробника NestJS. Він розширює можливості ArgumentsHost і дає змогу отримати:
тип транспорту запиту;
об’єкт HTTP-запиту;
об’єкт HTTP-відповіді;
клас контролера;
метод контролера, який зараз виконується;
аргументи поточного виклику.
ExecutionContext найчастіше використовується в:
guards;
interceptors;
деяких інших компонентах, яким потрібно аналізувати поточний запит або обробник.
Наприклад, guard може перевірити заголовки HTTP-запиту, а interceptor — додати інформацію до відповіді та визначити назву контролера й методу.
getType()Повертає тип транспорту:
const type = context.getType();Для HTTP-запиту результатом буде:
httpУ NestJS також можуть використовуватися інші типи транспорту, наприклад rpc або ws. Тому код, який працює тільки з HTTP, за потреби може перевіряти тип контексту.
switchToHttp()Перемикає контекст на HTTP і повертає об’єкт із методами для доступу до HTTP-даних:
const http = context.switchToHttp();Після цього доступні:
const request = http.getRequest();
const response = http.getResponse();
const next = http.getNext();Для Express запит і відповідь можна типізувати так:
import { Request, Response } from 'express';
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();getClass()Повертає клас контролера:
const controller = context.getClass();
console.log(controller.name);Якщо обробник належить UsersController, тоді controller.name матиме значення:
UsersControllerМетод повертає сам конструктор класу, а не його екземпляр.
getHandler()Повертає функцію поточного методу контролера:
const handler = context.getHandler();
console.log(handler.name);Якщо виконується метод findAll, тоді handler.name матиме значення:
findAllЦе корисно, коли компонент має відрізняти різні обробники або отримувати інформацію про конкретний маршрут.
getArgs()Повертає масив аргументів поточного виклику:
const args = context.getArgs();Для HTTP-контексту це зазвичай аргументи, передані фреймворком до обробника. На практиці для HTTP-запитів частіше використовують switchToHttp().getRequest(), оскільки цей спосіб зрозуміліший і типобезпечніший.
ExecutionContext у guardGuard реалізує інтерфейс CanActivate. Його метод canActivate отримує ExecutionContext.
Це дає змогу перевірити:
заголовки запиту;
cookies;
параметри;
HTTP-метод;
URL;
контролер і метод, які потрібно виконати.
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context
.switchToHttp()
.getRequest<Request>();
const controllerName = context.getClass().name;
const handlerName = context.getHandler().name;
console.log(
`Перевірка доступу: ${controllerName}.${handlerName}`,
);
const role = request.headers['x-role'];
if (role !== 'admin') {
throw new ForbiddenException('Потрібна роль admin');
}
return true;
}
}Guard виконується до методу контролера. Якщо він повертає false або викидає виняток, обробник контролера не буде викликаний.
ExecutionContext в interceptorInterceptor реалізує інтерфейс NestInterceptor. Його метод intercept отримує контекст і CallHandler.
CallHandler представляє наступний етап виконання — зазвичай сам метод контролера:
return next.handle();В interceptor можна:
прочитати дані запиту;
дізнатися назву класу та методу;
виконати код до виклику контролера;
виконати код після виклику контролера;
змінити результат через RxJS-оператори.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { tap } from 'rxjs/operators';
@Injectable()
export class RequestInfoInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
) {
const request = context
.switchToHttp()
.getRequest<Request>();
const response = context
.switchToHttp()
.getResponse<Response>();
const controllerName = context.getClass().name;
const handlerName = context.getHandler().name;
const startedAt = Date.now();
console.log(
`${request.method} ${request.url} -> ` +
`${controllerName}.${handlerName}`,
);
response.setHeader('X-Controller', controllerName);
response.setHeader('X-Handler', handlerName);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startedAt;
console.log(
`Обробку завершено за ${duration} мс`,
);
}),
);
}
}У цьому прикладі:
до виконання контролера читаються дані запиту;
визначаються клас контролера та його метод;
до HTTP-відповіді додаються заголовки;
після виконання контролера обчислюється тривалість запиту.
ExecutionContext у filterМетод catch у filter отримує ArgumentsHost, а не безпосередньо ExecutionContext:
catch(exception: unknown, host: ArgumentsHost) {
const http = host.switchToHttp();
}ArgumentsHost надає спільний інтерфейс для різних типів транспорту. Для HTTP потрібно викликати switchToHttp().
У filter можна отримати:
request;
response;
HTTP-статус;
URL запиту;
інші дані, необхідні для формування відповіді про помилку.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpErrorFilter
implements ExceptionFilter<HttpException>
{
catch(
exception: HttpException,
host: ArgumentsHost,
): void {
const http = host.switchToHttp();
const request = http.getRequest<Request>();
const response = http.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
method: request.method,
path: request.url,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}@Catch(HttpException) означає, що filter обробляє винятки типу HttpException та його нащадків, наприклад BadRequestException або ForbiddenException.
Нижче наведено приклад застосунку, у якому:
guard отримує заголовок x-role;
interceptor отримує request, response, клас і метод контролера;
filter формує власну HTTP-відповідь для винятків.
import {
ArgumentsHost,
BadRequestException,
CallHandler,
CanActivate,
Controller,
ExecutionContext,
ExceptionFilter,
ForbiddenException,
Get,
HttpException,
Injectable,
Module,
NestInterceptor,
RequestMethod,
UseGuards,
UseInterceptors,
Catch,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Request, Response } from 'express';
import { tap } from 'rxjs/operators';
@Injectable()
class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context
.switchToHttp()
.getRequest<Request>();
const controllerName = context.getClass().name;
const handlerName = context.getHandler().name;
console.log(
`Guard перевіряє ${controllerName}.${handlerName}`,
);
if (request.headers['x-role'] !== 'admin') {
throw new ForbiddenException(
'Для цього маршруту потрібна роль admin',
);
}
return true;
}
}
@Injectable()
class RequestInfoInterceptor
implements NestInterceptor
{
intercept(
context: ExecutionContext,
next: CallHandler,
) {
const request = context
.switchToHttp()
.getRequest<Request>();
const response = context
.switchToHttp()
.getResponse<Response>();
const controllerName = context.getClass().name;
const handlerName = context.getHandler().name;
const startedAt = Date.now();
console.log(
`${request.method} ${request.url} -> ` +
`${controllerName}.${handlerName}`,
);
response.setHeader('X-Controller', controllerName);
response.setHeader('X-Handler', handlerName);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startedAt;
console.log(
`Запит оброблено за ${duration} мс`,
);
}),
);
}
}
@Catch(HttpException)
class HttpErrorFilter
implements ExceptionFilter<HttpException>
{
catch(
exception: HttpException,
host: ArgumentsHost,
): void {
const http = host.switchToHttp();
const request = http.getRequest<Request>();
const response = http.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
method: request.method,
path: request.url,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
@Controller('demo')
@UseGuards(AdminGuard)
@UseInterceptors(RequestInfoInterceptor)
class DemoController {
@Get('hello')
hello(@Request() request: Request) {
if (request.query.fail === '1') {
throw new BadRequestException(
'Параметр fail активував помилку',
);
}
return {
message: 'Запит успішно оброблено',
userAgent: request.headers['user-agent'] ?? null,
};
}
}
@Module({
controllers: [DemoController],
providers: [
AdminGuard,
RequestInfoInterceptor,
],
})
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpErrorFilter());
await app.listen(3000);
}
bootstrap();Для перевірки прикладу:
curl http://localhost:3000/demo/helloЗапит буде відхилено guard, оскільки немає необхідного заголовка.
Запит із правильною роллю:
curl -H "x-role: admin" http://localhost:3000/demo/helloЗапит із помилкою в обробнику:
curl -H "x-role: admin" \
"http://localhost:3000/demo/hello?fail=1"У відповідь filter поверне JSON із полями statusCode, method, path, message і timestamp.
У спрощеному вигляді обробка HTTP-запиту має такий порядок:
guard отримує ExecutionContext і вирішує, чи дозволяти виконання;
interceptor отримує той самий контекст і може виконати код до обробника;
виконується метод контролера;
interceptor може обробити результат;
якщо виник виняток, його перехоплює filter.
Один і той самий ExecutionContext описує поточний виклик, але кожен компонент використовує його для власної задачі.
Для типового HTTP-коду достатньо такої схеми:
const type = context.getType();
if (type === 'http') {
const request = context
.switchToHttp()
.getRequest();
}Для інформації про маршрут:
const controller = context.getClass().name;
const handler = context.getHandler().name;Для HTTP-відповіді у filter або interceptor:
const response = context
.switchToHttp()
.getResponse();У filter використовується ArgumentsHost, тому аналогічна операція має вигляд:
const response = host
.switchToHttp()
.getResponse();Неправильно:
const request = context.getRequest();ExecutionContext не має методу getRequest() безпосередньо.
Правильно:
const request = context
.switchToHttp()
.getRequest();ExecutionContext може представляти не лише HTTP. Якщо компонент потенційно використовується для різних транспортів, спочатку перевіряйте:
if (context.getType() !== 'http') {
return true;
}context.getClass()повертає конструктор класу контролера. Для отримання його назви використовуйте:
context.getClass().nameМетод:
context.getHandler()повертає функцію обробника, а не рядок із назвою. Назву можна отримати через:
context.getHandler().nameМетоди response залежать від адаптера. Наприклад, у прикладі використовується:
response.status(status).json(body);Це Express-підхід. Якщо застосунок працює з іншим адаптером, потрібно враховувати його API.
ArgumentsHost і ExecutionContextGuards та interceptors отримують ExecutionContext:
canActivate(context: ExecutionContext) {}
intercept(context: ExecutionContext, next: CallHandler) {}Filters отримують ArgumentsHost:
catch(exception: unknown, host: ArgumentsHost) {}Для HTTP вони використовують однаковий принцип:
host.switchToHttp()або:
context.switchToHttp()ExecutionContext описує поточне виконання обробника NestJS.
getType() повертає тип транспорту.
switchToHttp() відкриває доступ до request, response і next.
getClass() повертає клас контролера.
getHandler() повертає метод поточного обробника.
У guard контекст допомагає перевірити доступ до маршруту.
В interceptor контекст використовується для аналізу запиту та контролю виконання обробника.
У filter через ArgumentsHost можна отримати HTTP-запит і сформувати відповідь про помилку.
Перед використанням HTTP-методів варто переконатися, що контекст справді має тип http.