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
3 Answers
Reset to default 4I 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.