Skip to content

Commit

Permalink
Merge pull request #11 from youngkiu/feature/refactor-HttpToGrpcInter…
Browse files Browse the repository at this point in the history
…ceptor

refactor: Generalize HttpToGrpcInterceptor with status code map.
  • Loading branch information
mohsenbostan authored Mar 24, 2024
2 parents ddb0a14 + da2df3a commit eae5684
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 30 deletions.
40 changes: 12 additions & 28 deletions lib/interceptors/http-to-grpc.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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<number, any> = {
[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 {
Expand All @@ -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,
}),
);
}),
);
}
Expand Down
20 changes: 20 additions & 0 deletions lib/utils/grpc-codes-map.ts
Original file line number Diff line number Diff line change
@@ -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<number, number> = {
[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,
};
1 change: 1 addition & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./error-object";
export * from "./http-codes-map";
export * from "./grpc-codes-map";
4 changes: 2 additions & 2 deletions test/interceptors/grpc-to-http-interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(),
Expand Down
75 changes: 75 additions & 0 deletions test/interceptors/http-to-grpc-interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -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<any> => {
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<any>;

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<any>;

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<any>;

intercept$.subscribe({
error: (err) => {
expect(err).toBeInstanceOf(RpcException);
expect(err.message).toEqual(NotFoundException.name);
done();
},
complete: () => done(),
});
});
});

0 comments on commit eae5684

Please sign in to comment.