diff --git a/lib/interceptors/http-to-grpc.interceptor.ts b/lib/interceptors/http-to-grpc.interceptor.ts index 0f8628d..16dccdd 100644 --- a/lib/interceptors/http-to-grpc.interceptor.ts +++ b/lib/interceptors/http-to-grpc.interceptor.ts @@ -1,35 +1,14 @@ import { CallHandler, ExecutionContext, - HttpStatus, Injectable, NestInterceptor, } from "@nestjs/common"; import { Observable, throwError } from "rxjs"; import { catchError } from "rxjs/operators"; -import { - GrpcAbortedException, - GrpcAlreadyExistsException, - GrpcInternalException, - GrpcInvalidArgumentException, - GrpcNotFoundException, - GrpcPermissionDeniedException, - GrpcResourceExhaustedException, - GrpcUnauthenticatedException, - GrpcUnknownException, -} from "../exceptions"; - -const GRPC_EXCEPTION_FROM_HTTP: Record = { - [HttpStatus.NOT_FOUND]: GrpcNotFoundException, - [HttpStatus.FORBIDDEN]: GrpcPermissionDeniedException, - [HttpStatus.METHOD_NOT_ALLOWED]: GrpcAbortedException, - [HttpStatus.INTERNAL_SERVER_ERROR]: GrpcInternalException, - [HttpStatus.TOO_MANY_REQUESTS]: GrpcResourceExhaustedException, - [HttpStatus.BAD_GATEWAY]: GrpcUnknownException, - [HttpStatus.CONFLICT]: GrpcAlreadyExistsException, - [HttpStatus.UNPROCESSABLE_ENTITY]: GrpcInvalidArgumentException, - [HttpStatus.UNAUTHORIZED]: GrpcUnauthenticatedException, -}; +import { GRPC_CODE_FROM_HTTP } from "../utils"; +import { status as Status } from "@grpc/grpc-js"; +import { RpcException } from "@nestjs/microservices"; @Injectable() export class HttpToGrpcInterceptor implements NestInterceptor { @@ -55,11 +34,16 @@ export class HttpToGrpcInterceptor implements NestInterceptor { message: string; }; - if (!(exception.statusCode in GRPC_EXCEPTION_FROM_HTTP)) - return throwError(() => err); + const statusCode = + GRPC_CODE_FROM_HTTP[exception.statusCode] || Status.INTERNAL; - const Exception = GRPC_EXCEPTION_FROM_HTTP[exception.statusCode]; - return throwError(() => new Exception(exception.message)); + return throwError( + () => + new RpcException({ + message: exception.message, + code: statusCode, + }), + ); }), ); } diff --git a/lib/utils/grpc-codes-map.ts b/lib/utils/grpc-codes-map.ts new file mode 100644 index 0000000..4af34bb --- /dev/null +++ b/lib/utils/grpc-codes-map.ts @@ -0,0 +1,20 @@ +import { status as Status } from "@grpc/grpc-js"; +import { HttpStatus } from "@nestjs/common"; + +// https://github.com/nestjs/nest/blob/master/packages/common/enums/http-status.enum.ts +export const GRPC_CODE_FROM_HTTP: Record = { + [HttpStatus.OK]: Status.OK, + [HttpStatus.BAD_GATEWAY]: Status.UNKNOWN, + [HttpStatus.UNPROCESSABLE_ENTITY]: Status.INVALID_ARGUMENT, + [HttpStatus.REQUEST_TIMEOUT]: Status.DEADLINE_EXCEEDED, + [HttpStatus.NOT_FOUND]: Status.NOT_FOUND, + [HttpStatus.CONFLICT]: Status.ALREADY_EXISTS, + [HttpStatus.FORBIDDEN]: Status.PERMISSION_DENIED, + [HttpStatus.TOO_MANY_REQUESTS]: Status.RESOURCE_EXHAUSTED, + [HttpStatus.PRECONDITION_REQUIRED]: Status.FAILED_PRECONDITION, + [HttpStatus.METHOD_NOT_ALLOWED]: Status.ABORTED, + [HttpStatus.PAYLOAD_TOO_LARGE]: Status.OUT_OF_RANGE, + [HttpStatus.NOT_IMPLEMENTED]: Status.UNIMPLEMENTED, + [HttpStatus.INTERNAL_SERVER_ERROR]: Status.INTERNAL, + [HttpStatus.UNAUTHORIZED]: Status.UNAUTHENTICATED, +}; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 36df824..4fee3f9 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,2 +1,3 @@ export * from "./error-object"; export * from "./http-codes-map"; +export * from "./grpc-codes-map"; diff --git a/test/interceptors/grpc-to-http-interceptor.spec.ts b/test/interceptors/grpc-to-http-interceptor.spec.ts index d8459af..7cdd582 100644 --- a/test/interceptors/grpc-to-http-interceptor.spec.ts +++ b/test/interceptors/grpc-to-http-interceptor.spec.ts @@ -1,4 +1,4 @@ -import { HttpException } from "@nestjs/common"; +import { HttpException, HttpStatus } from "@nestjs/common"; import { Observable, throwError } from "rxjs"; import { GrpcNotFoundException, GrpcToHttpInterceptor } from "../../lib"; @@ -48,7 +48,7 @@ describe("GrpcToHttpInterceptor", () => { intercept$.subscribe({ error: (err) => { expect(err).toBeInstanceOf(HttpException); - expect(err.status).toEqual(404); + expect(err.status).toEqual(HttpStatus.NOT_FOUND); done(); }, complete: () => done(), diff --git a/test/interceptors/http-to-grpc-interceptor.spec.ts b/test/interceptors/http-to-grpc-interceptor.spec.ts new file mode 100644 index 0000000..9cc1910 --- /dev/null +++ b/test/interceptors/http-to-grpc-interceptor.spec.ts @@ -0,0 +1,75 @@ +import { NotFoundException } from "@nestjs/common"; +import { Observable, throwError } from "rxjs"; +import { HttpToGrpcInterceptor } from "../../lib"; +import { RpcException } from "@nestjs/microservices"; +import { status as GrpcStatusCode } from "@grpc/grpc-js"; +import { GRPC_CODE_FROM_HTTP } from "../../lib/utils"; + +const throwMockException = ( + Exception: new (...args: any[]) => any, +): Observable => { + const exception = new Exception(Exception.name); + return throwError( + () => + new RpcException({ + message: exception.message, + code: GRPC_CODE_FROM_HTTP[exception.status], + }), + ); +}; + +describe("HttpToGrpcInterceptor", () => { + let interceptor: HttpToGrpcInterceptor; + + beforeAll(() => { + interceptor = new HttpToGrpcInterceptor(); + }); + + it("Should be defined", () => { + expect(interceptor).toBeDefined(); + }); + + it("Should convert HTTP exceptions to gRPC exceptions", (done) => { + const intercept$ = interceptor.intercept({} as any, { + handle: () => throwMockException(NotFoundException), + }) as Observable; + + intercept$.subscribe({ + error: (err) => { + expect(err).toBeInstanceOf(RpcException); + done(); + }, + complete: () => done(), + }); + }); + + it("Should convert HTTP exceptions to gRPC exceptions", (done) => { + const intercept$ = interceptor.intercept({} as any, { + handle: () => throwMockException(NotFoundException), + }) as Observable; + + intercept$.subscribe({ + error: (err) => { + expect(err).toBeInstanceOf(RpcException); + expect(err.error.code).toEqual(GrpcStatusCode.NOT_FOUND); + done(); + }, + complete: () => done(), + }); + }); + + it("Should contain the HTTP exception error message", (done) => { + const intercept$ = interceptor.intercept({} as any, { + handle: () => throwMockException(NotFoundException), + }) as Observable; + + intercept$.subscribe({ + error: (err) => { + expect(err).toBeInstanceOf(RpcException); + expect(err.message).toEqual(NotFoundException.name); + done(); + }, + complete: () => done(), + }); + }); +});