diff --git a/.changeset/gentle-kangaroos-cover.md b/.changeset/gentle-kangaroos-cover.md new file mode 100644 index 0000000..ed6891c --- /dev/null +++ b/.changeset/gentle-kangaroos-cover.md @@ -0,0 +1,5 @@ +--- +"nestjs-grpc-exceptions": minor +--- + +add grpc to http interceptor diff --git a/README.md b/README.md index 0d54f9d..b633d4a 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,24 @@ import { GrpcServerExceptionFilter } from "nestjs-grpc-exceptions"; export class UserModule {} ``` -Now you can use the exception classes: +Add the client interceptor to your client: ```ts +import { GrpcToHttpInterceptor } from 'nestjs-grpc-exceptions'; + +@Get(':id') +@UseInterceptors(GrpcToHttpInterceptor) +function findUser(@Param('id') id: number): void; +``` + +Now you can use the exception classes in your servers: + +```ts +import { + GrpcNotFoundException, + GrpcInvalidArgumentException, +} from "nestjs-grpc-exceptions"; + throw new GrpcNotFoundException("User Not Found."); throw new GrpcInvalidArgumentException("input 'name' is not valid."); ``` diff --git a/lib/filters/grpc-http-exception.filter.ts b/lib/filters/grpc-http-exception.filter.ts new file mode 100644 index 0000000..327aba6 --- /dev/null +++ b/lib/filters/grpc-http-exception.filter.ts @@ -0,0 +1,27 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpStatus, +} from "@nestjs/common"; +import { RpcException } from "@nestjs/microservices"; +import { HTTP_CODE_FROM_GRPC } from "../utils"; + +@Catch(RpcException) +export class GrpcHttpExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const details = JSON.parse(exception.details); + + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + const httpCode: number = HTTP_CODE_FROM_GRPC[Number(exception.code)] || 500; + const error = HttpStatus[httpCode]; + + response.json({ + message: details.error, + statusCode: httpCode, + error, + }); + } +} diff --git a/lib/filters/index.ts b/lib/filters/index.ts index b961d65..2d304ee 100644 --- a/lib/filters/index.ts +++ b/lib/filters/index.ts @@ -1 +1,2 @@ export * from "./grpc-server-exception.filter"; +export * from "./grpc-http-exception.filter"; diff --git a/lib/index.ts b/lib/index.ts index 9067e53..6ba7d3b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,2 +1,3 @@ export * from "./filters"; export * from "./exceptions"; +export * from "./interceptors"; diff --git a/lib/interceptors/grpc-to-http.interceptor.ts b/lib/interceptors/grpc-to-http.interceptor.ts new file mode 100644 index 0000000..480c02a --- /dev/null +++ b/lib/interceptors/grpc-to-http.interceptor.ts @@ -0,0 +1,61 @@ +import { + CallHandler, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, + NestInterceptor, +} from "@nestjs/common"; +import { RpcException } from "@nestjs/microservices"; +import { Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; +import { HTTP_CODE_FROM_GRPC } from "../utils"; + +@Injectable() +export class GrpcToHttpInterceptor implements NestInterceptor { + intercept( + _context: ExecutionContext, + next: CallHandler + ): Observable | Promise> { + return next.handle().pipe( + catchError((err) => { + if ( + !( + typeof err === "object" && + "details" in err && + err.details && + typeof err.details === "string" + ) + ) + throwError(() => err); + + const exception = JSON.parse(err.details) as { + error: string | object; + type: string; + exceptionName: string; + }; + + if (exception.exceptionName !== RpcException.name) + throwError(() => err); + + const statusCode = + HTTP_CODE_FROM_GRPC[200] || HttpStatus.INTERNAL_SERVER_ERROR; + + return throwError( + () => + new HttpException( + { + message: exception.error, + statusCode, + error: HttpStatus[statusCode], + }, + statusCode, + { + cause: err, + } + ) + ); + }) + ); + } +} diff --git a/lib/interceptors/index.ts b/lib/interceptors/index.ts new file mode 100644 index 0000000..b702b91 --- /dev/null +++ b/lib/interceptors/index.ts @@ -0,0 +1 @@ +export * from "./grpc-to-http.interceptor"; diff --git a/lib/utils/error-object.ts b/lib/utils/error-object.ts index 4aceb33..1698a7a 100644 --- a/lib/utils/error-object.ts +++ b/lib/utils/error-object.ts @@ -1,4 +1,5 @@ import type { status as GrpcStatusCode } from "@grpc/grpc-js"; +import { RpcException } from "@nestjs/microservices"; export type GrpcExceptionPayload = { message: string; @@ -19,6 +20,7 @@ export function errorObject( message: JSON.stringify({ error, type: typeof error === "string" ? "string" : "object", + exceptionName: RpcException.name, }), code, }; diff --git a/lib/utils/http-codes-map.ts b/lib/utils/http-codes-map.ts new file mode 100644 index 0000000..1ceb62c --- /dev/null +++ b/lib/utils/http-codes-map.ts @@ -0,0 +1,22 @@ +import { status as Status } from "@grpc/grpc-js"; +import { HttpStatus } from "@nestjs/common"; + +export const HTTP_CODE_FROM_GRPC: Record = { + [Status.OK]: HttpStatus.OK, + [Status.CANCELLED]: HttpStatus.METHOD_NOT_ALLOWED, + [Status.UNKNOWN]: HttpStatus.BAD_GATEWAY, + [Status.INVALID_ARGUMENT]: HttpStatus.UNPROCESSABLE_ENTITY, + [Status.DEADLINE_EXCEEDED]: HttpStatus.REQUEST_TIMEOUT, + [Status.NOT_FOUND]: HttpStatus.NOT_FOUND, + [Status.ALREADY_EXISTS]: HttpStatus.CONFLICT, + [Status.PERMISSION_DENIED]: HttpStatus.FORBIDDEN, + [Status.RESOURCE_EXHAUSTED]: HttpStatus.TOO_MANY_REQUESTS, + [Status.FAILED_PRECONDITION]: HttpStatus.PRECONDITION_REQUIRED, + [Status.ABORTED]: HttpStatus.METHOD_NOT_ALLOWED, + [Status.OUT_OF_RANGE]: HttpStatus.PAYLOAD_TOO_LARGE, + [Status.UNIMPLEMENTED]: HttpStatus.NOT_IMPLEMENTED, + [Status.INTERNAL]: HttpStatus.INTERNAL_SERVER_ERROR, + [Status.UNAVAILABLE]: HttpStatus.NOT_FOUND, + [Status.DATA_LOSS]: HttpStatus.INTERNAL_SERVER_ERROR, + [Status.UNAUTHENTICATED]: HttpStatus.UNAUTHORIZED, +}; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 87f19d8..36df824 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1 +1,2 @@ export * from "./error-object"; +export * from "./http-codes-map";