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 从服务器到客户端的自定义错误消息,因为 RpcExceptiongetError() 方法是 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,到您的可观察流并根据您的需要修改响应。