Jakub Vala
Jakub Vala

Reputation: 9

Error code from a server is not being represented correctly for the client in nest.js application

Hey I'm creating a grpc application using nest.js and I'm trying to throw an exception with status code 3 (INVALID_ARGUMENT) and 5 (NOT_FOUND) on grpc server and catch it on the client.

However, the error that I catch on the client is always with status code 2 (UNKNOWN).

I'm not sure what is going on, because it seems to me that the status code thrown on the server is not being mapped inside the gprc library (not sure though). I'm using observables inside my app but when I tried without throwing an error without observable I get the same result. In nest.js it's very specific and I'm not sure if it's not related to the version of some library, but what I'm reading from error is that it's an issue inside the grpc library and everything worked 2 days ago.

Here is an implementation of grpc server:

import { Controller } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { Metadata, ServerUnaryCall } from '@grpc/grpc-js';
import { Observable, throwError } from 'rxjs';
import { SAVED_TOKENS } from './saved-tokens';

@Controller()
export class GrpcServerTokenController {
    @GrpcMethod('TokenService', 'refreshToken')
    refreshToken(data: { token: string }, _metadata: Metadata, _call: ServerUnaryCall<any, any>): Observable<void> {
        if (!/^[A-Za-z0-9]{10,100}$/.test(data.token)) {
            return throwError(() => new RpcException({ code: 3, message: `Token ${data.token} is invalid.` }));
        }

        if (!SAVED_TOKENS.has(data.token)) {
            return throwError(() => new RpcException({ code: 5, message: `Token ${data.token} was not found.` }));
        }

        return new Observable((subscriber) => subscriber.next());
    }
}

Here is the implementation of the grpc client:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { status } from '@grpc/grpc-js';
import { lastValueFrom } from 'rxjs';
import { ClientGrpc } from '@nestjs/microservices';
import { TokenServiceInterface } from './token-service.interface';
import { InjectTokenPackage } from './app.tokens';
import { InvalidTokenException } from './exception/invalid-token.exception';
import { RefreshTokenServiceInterface } from './refresh-token-service.interface';
import { ServiceUnavailableException } from './exception/service-unavailable.exception';

@Injectable()
export class RefreshTokenService implements OnModuleInit, RefreshTokenServiceInterface {
    private tokenService!: TokenServiceInterface;

    constructor(
        @InjectTokenPackage()
        private readonly client: ClientGrpc,
    ) {}

    onModuleInit(): void {
        this.tokenService = this.client.getService<TokenServiceInterface>(
            'TokenService',
        );
    }

    async refreshToken(
        token: string,
    ): Promise<void> {
        try {
            const data = await lastValueFrom(this.tokenService.refreshToken({
                token,
            }));

            return data;
        } catch (error) {
            if ([status.INVALID_ARGUMENT, status.NOT_FOUND].includes(error.code)) {
                throw InvalidTokenException.create();
            }
            throw ServiceUnavailableException.create();
        }
    }
}

That's the error that I'm getting on the client side in the catch block:

Error: 2 UNKNOWN: Internal server error
    at callErrorFromStatus (/Users/<path_to_repo>/node_modules/.pnpm/@[email protected]/node_modules/@grpc/grpc-js/src/call.ts:81:17)
    at Object.onReceiveStatus (/Users/<path_to_repo>/node_modules/.pnpm/@[email protected]/node_modules/@grpc/grpc-js/src/client.ts:356:55)
    at Object.onReceiveStatus (/Users/<path_to_repo>/node_modules/.pnpm/@[email protected]/node_modules/@grpc/grpc-js/src/client-interceptors.ts:455:34)
    at Object.onReceiveStatus (/Users/<path_to_repo>/node_modules/.pnpm/@[email protected]/node_modules/@grpc/grpc-js/src/client-interceptors.ts:417:48)
    at /Users/<path_to_repo>/node_modules/.pnpm/@[email protected]/node_modules/@grpc/grpc-js/src/resolving-call.ts:111:24

I'm using the @gprc/grpc-js version 1.18.12 but I have the same issue with version 1.8.11

I tried a lot of things but nothing has worked so far, that's why I'm submitting a bug report. If it doesn't seem like a bug with @grpc/grpc-js library I'll close the issue.

Environment

Additional context

dependencies I'm using in my repo:

"dependencies": {
        "@grpc/grpc-js": "^1.8.11",
        "@grpc/proto-loader": "^0.7.5",
        "@hapi/joi": "^17.1.1",
        "@nestjs/common": "^9.0.0",
        "@nestjs/config": "^2.3.1",
        "@nestjs/core": "^9.0.0",
        "@nestjs/microservices": "^9.3.9",
        "@nestjs/platform-express": "^9.0.0",
        "@nestjs/platform-fastify": "9.1.2",
        "@nestjs/swagger": "^6.1.4",
        "@nestjs/terminus": "^9.2.1",
        "@types/express-xml-bodyparser": "^0.3.2",
        "@types/json2xml": "^0.1.1",
        "body-parser": "^1.20.1",
        "class-transformer": "^0.5.1",
        "class-validator": "^0.13.2",
        "express": "^4.18.2",
        "express-xml-bodyparser": "^0.3.0",
        "json2xml": "^0.1.3",
        "lodash": "^4.17.21",
        "reflect-metadata": "^0.1.13",
        "rimraf": "^3.0.2",
        "rxjs": "^7.8.0",
        "uuid": "^9.0.0",
        "xml2js": "^0.4.23",
        "xmlbuilder2": "^3.0.2"
    },
    "devDependencies": {
        "@nestjs/cli": "^9.0.0",
        "@nestjs/schematics": "^9.0.0",
        "@nestjs/testing": "^9.0.0",
        "@types/body-parser": "^1.19.2",
        "@types/express": "^4.17.15",
        "@types/express-xml-bodyparser": "^0.3.2",
        "@types/hapi__joi": "^17.0.0",
        "@types/jest": "28.1.4",
        "@types/json2xml": "^0.1.1",
        "@types/lodash": "^4.14.191",
        "@types/node": "^16.0.0",
        "@types/supertest": "^2.0.12",
        "@types/uuid": "^9.0.1",
        "@types/xml2js": "^0.4.11",
        "jest": "^29.3.1",
        "jest-serializer-xml": "^0.1.34",
        "socket.io-client": "^4.5.4",
        "source-map-support": "^0.5.21",
        "supertest": "^6.3.3",
        "ts-jest": "^29.0.3",
        "ts-loader": "^9.4.2",
        "ts-node": "^10.9.1",
        "tsconfig-paths": "^4.1.1",
        "tslib": "^2.4.1",
        "typescript": "~4.9.4",
        "utility-types": "^3.10.0",
        "webpack": "^5.0.0"
    }

I tried to throw just an RpcException but I got the same result or I tried to use a base Error, that didn't work either. I'm expecting to recieve an error with code 3 or 5 and not 2 UNKNOWN on the client side.

Upvotes: 0

Views: 1105

Answers (1)

Jakub Vala
Jakub Vala

Reputation: 9

Turned out I need to add an rpc exception filter so the error code maps correctly. The grpc server should look like this:


import { ArgumentsHost, Catch, Controller, RpcExceptionFilter, UseFilters } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { Metadata, ServerUnaryCall } from '@grpc/grpc-js';
import { Observable, throwError } from 'rxjs';
import { SAVED_TOKENS } from './saved-tokens';

@Catch(RpcException)
export class GrpcExceptionFilter implements RpcExceptionFilter<RpcException> {
    catch(exception: RpcException, host: ArgumentsHost): any {
        return throwError(() => exception.getError());
    }
}

@Controller()
export class GrpcServerTokenController {
    @GrpcMethod('TokenService', 'refreshToken')
    @UseFilters(GrpcExceptionFilter)
    refreshToken(data: { token: string }, _metadata: Metadata, _call: ServerUnaryCall<any, any>): Observable<void> {
        if (!/^[A-Za-z0-9]{10,100}$/.test(data.token)) {
            return throwError(() => new RpcException({ code: 3, message: `Token ${data.token} is invalid.` }));
        }

        if (!SAVED_TOKENS.has(data.token)) {
            return throwError(() => new RpcException({ code: 5, message: `Token ${data.token} was not found.` }));
        }

        return new Observable((subscriber) => subscriber.next());
    }
}

Upvotes: 0

Related Questions