NestJs 将 GRPC 异常转换为 HTTP 异常
NestJs transform GRPC exception to HTTP exception
我有一个通过 GRPC 连接到网关的 HTTP 服务器。网关还连接到其他 . GRPC 微服务。流程如下所示:
客户端 -> HttpServer -> GRPC 服务器(网关) -> GRPC 微服务服务器 X
我目前处理错误的方式是这样的(如果有更好的做法,请告诉我)为了简洁起见,我只会显示 nessaccery 代码
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)
}
}
我已经被困在同一个地方有一段时间了。似乎有效的是只有您作为消息发送的字符串才会在 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();
}
我能够创建并 return 从服务器到客户端的自定义错误消息,因为 RpcException
的 getError()
方法是 string | object
类型,它的实际对象在运行时构建。这是我的实现方式
微服务 X
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-util
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,
});
}
}
并且在控制器中,我使用 pipe
方法从 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
,您可能已经看到客户端(使用我们的微服务的东西)returns 是一个可观察对象,这本质上意味着您可以应用其他运算符,我在这里使用pipe
,到您的可观察流并根据您的需要修改响应。
我有一个通过 GRPC 连接到网关的 HTTP 服务器。网关还连接到其他 . GRPC 微服务。流程如下所示:
客户端 -> HttpServer -> GRPC 服务器(网关) -> GRPC 微服务服务器 X
我目前处理错误的方式是这样的(如果有更好的做法,请告诉我)为了简洁起见,我只会显示 nessaccery 代码
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)
}
}
我已经被困在同一个地方有一段时间了。似乎有效的是只有您作为消息发送的字符串才会在 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();
}
我能够创建并 return 从服务器到客户端的自定义错误消息,因为 RpcException
的 getError()
方法是 string | object
类型,它的实际对象在运行时构建。这是我的实现方式
微服务 X
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-util
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,
});
}
}
并且在控制器中,我使用 pipe
方法从 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
,您可能已经看到客户端(使用我们的微服务的东西)returns 是一个可观察对象,这本质上意味着您可以应用其他运算符,我在这里使用pipe
,到您的可观察流并根据您的需要修改响应。