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, в ваш наблюдаемый поток и изменить ответ в соответствии с вашими потребностями.

Другие вопросы по тегам