NestJs преобразовывают исключение GRPC в исключение HTTP
У меня есть HTTP-сервер, который подключается к шлюзу через GRPC. шлюз также соединяется с другими. Микросервисы GRPC. поток выглядит так:
Клиент -> HttpServer -> Сервер GRPC (шлюз) -> Сервер микросервисов GRPC X
В настоящее время я обрабатываю ошибки так (пожалуйста, дайте мне знать, если есть лучшая практика), я буду показывать только несессерский код для краткости
Сервер микросервисов GRPC X
@GrpcMethod() get(clientDetails: Records.UserDetails.AsObject): Records.RecordResponse.AsObject {
this.logger.log("Get Record for client");
throw new RpcException({message: 'some error', code: status.DATA_LOSS})
}
это просто выдает ошибку для клиента GRPC (который отлично работает)
Сервер GRPC
@GrpcMethod() async get(data: Records.UserDetails.AsObject, metaData): Promise<Records.RecordResponse.AsObject> {
try {
return await this.hpGrpcRecordsService.get(data).toPromise();
} catch(e) {
throw new RpcException(e)
}
}
Сервер Grpc улавливает ошибку, которая, в свою очередь, улавливается покупкой глобального обработчика исключений (это отлично работает)
@Catch(RpcException)
export class ExceptionFilter implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
if( Object.prototype.hasOwnProperty.call(exception, 'message') &&
Object.prototype.hasOwnProperty.call(exception.message, 'code') &&
exception.message.code === 2
){
exception.message.code = 13
}
return throwError(exception.getError());
}
}
Это возвращает ошибку на сервер Http (клиент grpc, отлично работает)
Теперь, когда он доходит до Http-сервера, я надеялся, что смогу настроить другой обработчик исключений RPC и преобразовать ошибку в HTTP except. но я не уверен, возможно ли это, я использую Nest только несколько дней и еще не полностью понимаю это.
Вот пример того, что я надеялся сделать (код не работает, просто пример того, что я хочу). id предпочитает глобально перехватывать исключения, а не иметь повсюду блоки try/catch
@Catch(RpcException)
export class ExceptionFilter implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
//Map UNKNOWN(2) grpc error to INTERNAL(13)
if( Object.prototype.hasOwnProperty.call(exception, 'message') &&
Object.prototype.hasOwnProperty.call(exception.message, 'code') &&
exception.message.code === 2
){ exception.message.code = 13 }
throw new HttpException('GOT EM', HttpStatus.BAD_GATEWAY)
}
}
3 ответа
Я застрял на одном и том же месте уже некоторое время. Кажется, что работает то, что только строка, которую вы отправляете в качестве сообщения, получает на HTTP-сервере. Таким образом, приведенный ниже код работает как фильтр на HTTP-сервере, но вы должны проверять статус через строку сообщения.
@Catch(RpcException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: RpcException, host: ArgumentsHost) {
const err = exception.getError();
// console.log(err);
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
response
.json({
message: err["details"],
code: err['code'],
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
if(err['details'] === UserBusinessErrors.InvalidCredentials.message){
this.logger.error(e);
throw new HttpException( UserBusinessErrors.InvalidCredentials.message, 409)
} else {
this.logger.error(e);
throw new InternalServerErrorException();
}
Я смог создать и вернуть пользовательское сообщение об ошибке с сервера на клиент, так как
RpcException
х
getError()
метод имеет тип
string | object
, его фактический объект создается во время выполнения. Вот как выглядит моя реализация
Микросервис Х
import { status } from '@grpc/grpc-js';
import { Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { CreateUserRequest, CreateUserResponse } from 'xxxx';
interface CustomExceptionDetails {
type: string;
details: string,
domain: string,
metadata: { service: string }
}
@Injectable()
export class UsersService {
users: CreateUserResponse[] = [];
findOneById(id: string) {
return this.users.find(e => e.id === id);
}
createUser(request: CreateUserRequest) {
// verify if user already exists
const userExists = this.findOneById(request.email);
if (userExists) {
const exceptionStatus = status.ALREADY_EXISTS;
const details = <CustomExceptionDetails>{
type: status[exceptionStatus],
details: 'User with with email already exists',
domain: 'xapis.com',
metadata: {
service: 'X_MICROSERVICE'
}
};
throw new RpcException({
code: exceptionStatus,
message: JSON.stringify(details) // note here (payload is stringified)
});
}
// create user
const user = <CreateUserResponse>{
id: request.email,
firstname: request.firstname,
lastname: request.lastname,
phoneNumber: request.phoneNumber,
email: request.email,
};
this.users.push(user);
return user;
}
}
Сервер шлюза Y (HttpExceptionFilter)
import { ArgumentsHost, Catch, ExceptionFilter, HttpException,
HttpStatus } from "@nestjs/common";
import { RpcException } from "@nestjs/microservices";
import { Request, Response } from 'express';
import { ErrorStatusMapper } from "../utils/error-status-mapper.util";
import { Metadata, status } from '@grpc/grpc-js';
interface CustomExceptionDetails {
type: string;
details: string,
domain: string,
metadata: { service: string }
}
interface CustomException<T> {
code: status;
details: T;
metadata: Metadata;
}
@Catch(RpcException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: RpcException, host: ArgumentsHost) {
const err = exception.getError();
let _exception: CustomException<string>;
let details: CustomExceptionDetails;
if (typeof err === 'object') {
_exception = err as CustomException<string>;
details = <CustomExceptionDetails>(JSON.parse(_exception.details));
}
// **You can log your exception details here**
// log exception (custom-logger)
const loggerService: LoggerService<CustomExceptionDetails> =
new LoggerService(FeatureService["CLIENT/UserAccountService"]);
loggerService.log(<LogData<CustomExceptionDetails>>{ type: LogType.ERROR, data: details });
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
// const request = ctx.getRequest<Request>();
const mapper = new ErrorStatusMapper();
const status = mapper.grpcToHttpMapper(_exception.code);
const type = HttpStatus[status];
response
.status(status)
.json({
statusCode: status,
message: details.details,
error: type,
});
}
}
ErrorStatusMapper-утилита
import { status } from '@grpc/grpc-js';
import { Status } from "@grpc/grpc-js/build/src/constants";
import { HttpStatus, Injectable } from "@nestjs/common";
@Injectable()
export class ErrorStatusMapper {
grpcToHttpMapper(status: status): HttpStatus {
let httpStatusEquivalent: HttpStatus;
switch (status) {
case Status.OK:
httpStatusEquivalent = HttpStatus.OK;
break;
case Status.CANCELLED:
httpStatusEquivalent = HttpStatus.METHOD_NOT_ALLOWED;
break;
case Status.UNKNOWN:
httpStatusEquivalent = HttpStatus.BAD_GATEWAY;
break;
case Status.INVALID_ARGUMENT:
httpStatusEquivalent = HttpStatus.UNPROCESSABLE_ENTITY;
break;
case Status.DEADLINE_EXCEEDED:
httpStatusEquivalent = HttpStatus.REQUEST_TIMEOUT;
break;
case Status.NOT_FOUND:
httpStatusEquivalent = HttpStatus.NOT_FOUND;
break;
case Status.ALREADY_EXISTS:
httpStatusEquivalent = HttpStatus.CONFLICT;
break;
case Status.PERMISSION_DENIED:
httpStatusEquivalent = HttpStatus.FORBIDDEN;
break;
case Status.RESOURCE_EXHAUSTED:
httpStatusEquivalent = HttpStatus.TOO_MANY_REQUESTS;
break;
case Status.FAILED_PRECONDITION:
httpStatusEquivalent = HttpStatus.PRECONDITION_REQUIRED;
break;
case Status.ABORTED:
httpStatusEquivalent = HttpStatus.METHOD_NOT_ALLOWED;
break;
case Status.OUT_OF_RANGE:
httpStatusEquivalent = HttpStatus.PAYLOAD_TOO_LARGE;
break;
case Status.UNIMPLEMENTED:
httpStatusEquivalent = HttpStatus.NOT_IMPLEMENTED;
break;
case Status.INTERNAL:
httpStatusEquivalent = HttpStatus.INTERNAL_SERVER_ERROR;
break;
case Status.UNAVAILABLE:
httpStatusEquivalent = HttpStatus.NOT_FOUND;
break;
case Status.DATA_LOSS:
httpStatusEquivalent = HttpStatus.INTERNAL_SERVER_ERROR;
break;
case Status.UNAUTHENTICATED:
httpStatusEquivalent = HttpStatus.UNAUTHORIZED;
break;
default:
httpStatusEquivalent = HttpStatus.INTERNAL_SERVER_ERROR;
break;
}
return httpStatusEquivalent;
}
}
У меня такая же проблема. Затем я нашел решение, которое работает для меня.
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
success: false,
statusCode: status,
message: exception.message,
path: request.url,
});
}
}
и в контроллере я использую метод, чтобы поймать ошибку службы GRPC как
@Post('/register')
@Header('Content-Type', 'application/json')
async registerUser(@Body() credentials: CreateUserDto) {
return this.usersService.Register(credentials).pipe(
catchError((val) => {
throw new HttpException(val.message, 400);
}),
);
}
Если вы знакомы с
RxJS
вы, наверное, уже видели, что клиент (то, что потребляет наш микросервис) возвращает наблюдаемое, что, по сути, означает, что вы можете применять другие операторы, здесь я использовал
pipe
, в ваш наблюдаемый поток и изменить ответ в соответствии с вашими потребностями.