最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - NestJs transform GRPC exception to HTTP exception - Stack Overflow

programmeradmin4浏览0评论

I have a HTTP server that connects to a gateway over GRPC. the gateway also connects to other . GRPC microservices. the flow looks like this:

Client -> HttpServer -> GRPC server (gateway) -> GRPC microservice server X

The way i handle errors currently is like so (please let me know if there is better practice) i will only show nessaccery code for brevity

GRPC microservice server 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})
  }

this simple throws an error to the GRPC client (which works fine)

GRPC Server

  @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 server catches the error which is in turn caught buy the global exception handler (this works fine)

@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());
  }
}

This throws the error back to the Http server (grpc client, works fine)

Now when it gets to the Http server i was hoping i could set up another RPC exception handler and transform the error into a HTTP except. but i'm unsure if it is possible, i have only been using nest for a few days and am yet to full understand it.

Here is an example of what i was hoping to do (code is not working, just example of what i want). id prefer to globally catch the exceptions rather than have try/catch blocks everywhere

@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)
  }
}

I have a HTTP server that connects to a gateway over GRPC. the gateway also connects to other . GRPC microservices. the flow looks like this:

Client -> HttpServer -> GRPC server (gateway) -> GRPC microservice server X

The way i handle errors currently is like so (please let me know if there is better practice) i will only show nessaccery code for brevity

GRPC microservice server 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})
  }

this simple throws an error to the GRPC client (which works fine)

GRPC Server

  @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 server catches the error which is in turn caught buy the global exception handler (this works fine)

@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());
  }
}

This throws the error back to the Http server (grpc client, works fine)

Now when it gets to the Http server i was hoping i could set up another RPC exception handler and transform the error into a HTTP except. but i'm unsure if it is possible, i have only been using nest for a few days and am yet to full understand it.

Here is an example of what i was hoping to do (code is not working, just example of what i want). id prefer to globally catch the exceptions rather than have try/catch blocks everywhere

@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)
  }
}
Share Improve this question asked Feb 17, 2020 at 17:40 Jay PoveyJay Povey 7672 gold badges7 silver badges15 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 4

I have been stuck at the same place for some time now. What seems to work is that only the string you send as message gets received at the HTTP server. So the code below as a filter in HTTP server works, but you have to check for status via the message string.

@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();
 }

I was able to create and return a custom error message from server to client since RpcException's getError() method is of type string | object, its actual object is constructed at runtime. Here's what my implementation looks like

Microservice X

import { status } from '@grpc/grpc-js';
import { Injectable } from '@nestjs/mon';
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.',
                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;
    }
}

Gateway Y Server (HttpExceptionFilter)

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, 
HttpStatus } from "@nestjs/mon";
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/mon";

@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;
    }
 }

I have the same problem. Then I found a solution which works for me.

    @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,
        });
      }
    }

and in the controller, I use pipe method to catch the error from the GRPC service as

  @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);
      }),
    );
  }

If you’re familiar with RxJS you probably already saw that the client (what consumes our microservice) returns an observable, what it essentialy means you can apply other operators, here I used pipe, to your observable stream and modify response to your needs.

发布评论

评论列表(0)

  1. 暂无评论