Is there any way to use Pipes with Grpc in Nestjs? - nestjs

So I'm building an httpgateway which sends messages to a microservice made with nestjs/grpc.
Problem is that, once I decorate my controller with #UsePipes(....) it throws an error for the gateway. I tried to log data entering into pipe and found out that grpc sends not only payload but also metadata and ServerDuplexStream prior to payload itself. So, my consumer throws an error because it faces with the ServerDuplexStream at first and cannot validate the args inside it.
I further tried to use my pipes in app.service but it doesnt make any sense since pipes receive data from the request. So it doesnt work as expected.
Is there a workaround like putting all three in a call in my gateway prior to sending request?
You can see an example of a pipe that im trying to implement:
#Injectable()
export class ValidateSingleBalanceByUser implements PipeTransform {
transform(value: SingleBalanceDto) {
if (!value.user) throw new RpcException('Provide user value to query!');
if (!value.asset) throw new RpcException('Provide asset value to query!');
return value;
}
}
and an example of a controller that im trying to implement to
#UsePipes(new ValidateSingleBalanceByUser())
#GrpcMethod('BridgeService', 'getSingleBalanceByUser')
singleBalanceByUser(data: SingleBalanceDto): Promise<Balance> {
return this.balancesService.handleSingleBalanceByUser(data);
}

I was getting the same problem, cause the Validation its a pipe and does not treat grpc exceptions. I fixed it using this solution:
controller.ts
#GrpcMethod('service','action')
#UsePipes(new ValidationPipe({ transform: true }))
#UseFilters(new TranslateHttpToGrpcExceptionFilter())
method(){}
translate.ts
#Catch(HttpException)
export class TranslateHttpToGrpcExceptionFilter implements ExceptionFilter {
static HttpStatusCode: Record<number, number> = {
[HttpStatus.BAD_REQUEST]: status.INVALID_ARGUMENT,
[HttpStatus.UNAUTHORIZED]: status.UNAUTHENTICATED,
[HttpStatus.FORBIDDEN]: status.PERMISSION_DENIED,
[HttpStatus.NOT_FOUND]: status.NOT_FOUND,
[HttpStatus.CONFLICT]: status.ALREADY_EXISTS,
[HttpStatus.GONE]: status.ABORTED,
[HttpStatus.TOO_MANY_REQUESTS]: status.RESOURCE_EXHAUSTED,
499: status.CANCELLED,
[HttpStatus.INTERNAL_SERVER_ERROR]: status.INTERNAL,
[HttpStatus.NOT_IMPLEMENTED]: status.UNIMPLEMENTED,
[HttpStatus.BAD_GATEWAY]: status.UNKNOWN,
[HttpStatus.SERVICE_UNAVAILABLE]: status.UNAVAILABLE,
[HttpStatus.GATEWAY_TIMEOUT]: status.DEADLINE_EXCEEDED,
[HttpStatus.HTTP_VERSION_NOT_SUPPORTED]: status.UNAVAILABLE,
[HttpStatus.PAYLOAD_TOO_LARGE]: status.OUT_OF_RANGE,
[HttpStatus.UNSUPPORTED_MEDIA_TYPE]: status.CANCELLED,
[HttpStatus.UNPROCESSABLE_ENTITY]: status.CANCELLED,
[HttpStatus.I_AM_A_TEAPOT]: status.UNKNOWN,
[HttpStatus.METHOD_NOT_ALLOWED]: status.CANCELLED,
[HttpStatus.PRECONDITION_FAILED]: status.FAILED_PRECONDITION
}
catch(exception: HttpException): Observable<never> | void {
const httpStatus = exception.getStatus()
const httpRes = exception.getResponse() as { details?: unknown, message: unknown }
return throwError(() => ({
code: TranslateHttpToGrpcExceptionFilter.HttpStatusCode[httpStatus] ?? status.UNKNOWN,
message: httpRes.message || exception.message,
details: Array.isArray(httpRes.details) ? httpRes.details : httpRes.message
}))
}
}
Hope it helps!

Related

how can I do handling in nestjs when not caught

I am using nestjs.
I have created an AllExceptionFilter.
However, once I run the post request api on the httpService and then an error is returned
nestjs will not accept the RESTful API after that.
What should I do?
■ error log
/Users/username/Documents/workspace/project/nestjs/src/shared/filters/custom-exception.filter.ts:29
path: httpAdapter.getRequestUrl(ctx.getRequest()),
^
TypeError: Cannot read properties of undefined (reading 'getRequestUrl')
at AllExceptionsFilter.catch (/Users/username/Documents/workspace/project/nestjs/src/shared/filters/custom-exception.filter.ts:29:25)
at ExceptionsHandler.invokeCustomFilters (/Users/username/Documents/workspace/project/nestjs/node_modules/#nestjs/core/exceptions/exceptions-handler.js:33:26)
at ExceptionsHandler.next (/Users/username/Documents/workspace/project/nestjs/node_modules/#nestjs/core/exceptions/exceptions-handler.js:13:18)
at /Users/username/Documents/workspace/project/nestjs/node_modules/#nestjs/core/router/router-proxy.js:13:35
at processTicksAndRejections (node:internal/process/task_queues:96:5)
■ no try/catch point code
await firstValueFrom(this.httpService.post(
url,
{
id: 'id'
},
));
■ AllExceptionFilter
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '#nestjs/common';
import { HttpAdapterHost } from '#nestjs/core';
#Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
// In certain situations `httpAdapter` might not be available in the
// constructor method, thus we should resolve it here.
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
};
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
Looks like an error is thrown by your endpoint, and the exception filter has an error that is triggered when attempting to handle the first exception.
As specified in the stack trace, the error is coming from: custom-exception.filter.ts:29, which is this line: path: httpAdapter.getRequestUrl(ctx.getRequest())
The problem is that httpAdapter in that line is undefined. For some reason DI isn't injecting it.
If you're using this filter globally, note the following from the docs:
Global-scoped filters are used across the whole application, for every controller and every route handler. In terms of dependency injection, global filters registered from outside of any module (with useGlobalFilters() as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can register a global-scoped filter directly from any module using the following construction:
import { Module } from '#nestjs/common';
import { APP_FILTER } from '#nestjs/core';
#Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
If you use the above approach you won't need the useGlobalFilters() method.
If you want to bind this filter at the controller level, the following syntax enables DI:
#UseFilters(AllExceptionsFilter)
#Controller("app")
export class AppController {

Cannot catch BadRequestException thrown by FilesInterceptor

I am trying to set up a logic to save uploaded files using Multer.
To do this, I follow the Nestjs tutorial and use the "FilesInterceptor" interceptor.
controller file :
import {
Controller,
FileValidator,
ParseFilePipe,
Post,
UploadedFiles,
UseInterceptors
} from '#nestjs/common';
import { FilesInterceptor } from '#nestjs/platform-express';
import { Public } from 'src/auth/decorators/public.decorator';
import { MimeTypeValidationPipe } from './pipes/mimetype.validation.pipe';
const ACCEPTED_FILE_MIMETYPES = ["image/jpeg", "image/jpg", "image/png"]
const validators: FileValidator[] = [
new MimeTypeValidationPipe({ accepted: ACCEPTED_FILE_MIMETYPES })
];
#Controller('uploads')
export class UploadsController {
#Public()
#Post("/")
#UseInterceptors(FilesInterceptor("files"))
public async create(
#UploadedFiles(new ParseFilePipe({ validators }))
files: Express.Multer.File[]
){
files[0].originalname
const filenames = files.map(({ originalname }) => originalname)
return { filenames };
}
}
However, when I test the behavior of the server when the number of uploaded files exceeds the limit, the server returns me an error 500 (As if the error was not handled).
I then try to catch it by using an ExcepetionFilter like this one:
#Catch()
class TestFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
console.debug(exception)
if(exception instanceof HttpException) console.debug("This is an HTTP Exception !");
else console.debug("This is NOT an HTTP Exception");
const response = host.switchToHttp().getResponse<Response>();
return response.status(500).json({statusCode: 500 , message: "ERROR" });
}
}
And i get the following output :
BadRequestException: Too many files
at transformException (~/development/Nest/nestapi/node_modules/#nestjs/platform-express/multer/multer/multer.utils.js:19:20)
at ~/development/Nest/nestapi/node_modules/#nestjs/platform-express/multer/interceptors/files.interceptor.js:18:73
at ~/development/Nest/nestapi/node_modules/#nestjs/platform-express/node_modules/multer/lib/make-middleware.js:53:37
at AsyncResource.runInAsyncScope (node:async_hooks:202:9)
at listener (~/development/Nest/nestapi/node_modules/#nestjs/platform-express/node_modules/on-finished/index.js:170:15)
at onFinish (~/development/Nest/nestapi/node_modules/#nestjs/platform-express/node_modules/on-finished/index.js:101:5)
at callback (~/development/Nest/nestapi/node_modules/#nestjs/platform-express/node_modules/ee-first/index.js:55:10)
at IncomingMessage.onevent (~/development/Nest/nestapi/node_modules/#nestjs/platform-express/node_modules/ee-first/index.js:93:5)
at IncomingMessage.emit (node:events:539:35)
at endReadableNT (node:internal/streams/readable:1345:12) {
response: { statusCode: 400, message: 'Too many files', error: 'Bad Request' },
status: 400
}
This is NOT an HTTP Exception
The filter indicates that it is NOT an HTTPException.
However, while digging in the FilesInterceptor.ts code I notice that the caught errors are handled by a small utility function "transformException" which is supposed to transform the Multer error into an HttpException (depending on the error code returned by Multer)
multer.utils.ts file (from nest repo)
import {
BadRequestException,
HttpException,
PayloadTooLargeException,
} from '#nestjs/common';
import { multerExceptions } from './multer.constants';
export function transformException(error: Error | undefined) {
if (!error || error instanceof HttpException) {
return error;
}
switch (error.message) {
case multerExceptions.LIMIT_FILE_SIZE:
return new PayloadTooLargeException(error.message);
case multerExceptions.LIMIT_FILE_COUNT:
case multerExceptions.LIMIT_FIELD_KEY:
case multerExceptions.LIMIT_FIELD_VALUE:
case multerExceptions.LIMIT_FIELD_COUNT:
case multerExceptions.LIMIT_UNEXPECTED_FILE:
case multerExceptions.LIMIT_PART_COUNT:
return new BadRequestException(error.message);
}
return error;
}
I don't understand why my filter (and NestJS' unhandledExceptionFilter) can't detect this exception, since for me it is supposed to be an instance of HttpException.
Can you help me?
Best regards
You probably have 2 copies of #nestjs/common being included in your project. The code that creates the error is using one copy, and your exception filter is using the other copy. When your code is checking instanceof, it's checking to see if the exception is an instance of HttpException from it's copy of #nestjs/common, but it's not, it's an instance of HttpException from the other copy of #nestjs/common. This is known as "multiple realms" (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof#instanceof_and_multiple_realms).
The way to fix this is to make sure you only have 1 copy of #nestjs/common in your project. Often, the reason you have 2 is because you have 2 package.json files with different version specs that they call for (e.g. "#nestjs/common": "^8.0.0" in one package.json, and "#nestjs/common": "^9.0.0" in another). You may need to use e.g. the overrides key to force a dependency to use the same version that you use elsewhere.
Hope that helps!
Sorry!
I think the problem is with me.
The LTS version (1.4.4-lts.1) of multer is buggy. So I decided to downgrade to 1.4.4 (version in which the bug in question does not occur). But to do so, I had to downgrade the nested dependency manually by doing npm install multer#1.4.4 in the node_modules/#nest/platform-express directory.
But that's when nestjs starts to format my errors badly.
The funny thing is that going back (npm install multer#1.4.4-lts.1 to the node_modules/#nest/platform-express directory), it doesn't solve the problem (Errors are still badly handled) and I have to delete the node_modules/#nest/platform-express folder and reinstall the package from the root of the project to get everything back in order (But with the LTS version bug, of course).
It's weird.

Why can't return view in error filter in nestjs using fastify as underlying?

I wrote my own error filter based on the way given in the nestjs documentation:
#Catch()
export class AppExceptionfilter implements ExceptionFilter{
catch(exception: any, host: ArgumentsHost) {
return host.switchToHttp().getResponse().view('error')
}
}
But it doesn't return anything when error happens. I have tried to return error view in controllers and it returns normally. I don't know why it happens.

Custom TypeORM errors in NestJS Service

I am playing around with NestJS and I would like to take the error thrown from TypeORM and convert it into a shape that I can control.
Right now, I'm just trying to catch the error thrown from TypeORM and log it out to see that my custom filter is working correctly. But unfortunately, my console.log statement in the filter is never logging.
Here is a slimmed down version of my service and filter
user.service.ts
export class UserService {
constructor(
#InjectRepository(Users)
private readonly userRepository: Repository<Users>,
) {}
#UseFilters(new TypeOrmFilter())
async create(createUserDto: CreateUserDto) {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
}
type-orm-filter.ts
#Catch()
export class TypeOrmFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
console.log('\nI have caught an error\n', exception);
throw exception;
}
}
Here is the log output from the error being thrown by TypeORM
[Nest] 61496 - 04/11/2021, 9:01:42 PM [ExceptionsHandler] invalid input syntax for type uuid: "123e4567" +2482ms
QueryFailedError: invalid input syntax for type uuid: "123e4567"
at new QueryFailedError (my-nest-project/error-project-nestjs/node_modules/typeorm/error/QueryFailedError.js:11:28)
at PostgresQueryRunner.<anonymous> (my-nest-project/error-project-nestjs/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:247:31)
at step (my-nest-project/error-project-nestjs/node_modules/typeorm/node_modules/tslib/tslib.js:141:27)
at Object.throw (my-nest-project/error-project-nestjs/node_modules/typeorm/node_modules/tslib/tslib.js:122:57)
at rejected (my-nest-project/error-project-nestjs/node_modules/typeorm/node_modules/tslib/tslib.js:113:69)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
You missed the await in UserService#create :p
tip: If you configure ESLint properly, this might never happen again because you already marked that method with await (https://eslint.org/docs/rules/require-await). Or just enforce typing the return (https://github.com/typescript-eslint/typescript-eslint/blob/v3.10.1/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md)

NodeJs import issue with Jest

Issue on facebook/jest
Currently working on a project where I need to dynamically load other components in the main application system, I'm facing an issue with some error that throws a Jest Invariant constraint violation when trying to import dynamically a module.
An example is accessible at https://gitlab.com/matthieu88160/stryker-issue
In this project, I have a class used to load a component and another that will use the ComponentLoader and add some check and post-processing (this part of the code is not present in the accessible project).
In the GitLab job, two test suites are executed and one fails. My problem is that the two test suites are exactly the same, obviously, I copy-pasted the first one to create. The second one and by the way demonstrate the issue.
I already tried some workaround found on the web without any success.
Here is the code of the component I try to test :
import {existsSync} from 'fs';
import {format} from 'util';
export default class ComponentLoader
{
static load(file) {
if(existsSync(file)) {
return import(file);
}
throw new Error(
format('Component file "%s" does not exist. Cannot load module', file)
);
}
}
And the test itself:
import {describe, expect, test} from '#jest/globals';
import {format} from 'util';
import {fileURLToPath} from 'url';
import {dirname} from 'path';
import mock from 'jest-mock';
describe(
'ComponentLoader',
() => {
describe('load', () => {
test('load method is able to load a component from a file', () => new Promise(
resolve => {
Promise.all(
[import('../src/ComponentLoader.mjs'), import('./fixture/component/A1/component.fixture.mjs')]
).then(modules => {
const file = format(
'%s/./fixture/component/A1/component.fixture.mjs',
dirname(fileURLToPath(import.meta.url))
);
modules[0].default.load(file).then(obj => {
expect(obj).toBe(modules[1]);
resolve();
});
});
})
);
});
}
);
And here the error report:
PASS test/ComponentLoaderA.test.mjs
FAIL test/ComponentLoaderB.test.mjs
● ComponentLoader › load › load method is able to load a component from a file
15 | static load(file) {
16 | if(existsSync(file)) {
> 17 | return import(file);
| ^
18 | }
19 |
20 | throw new Error(
at invariant (node_modules/jest-runtime/build/index.js:2004:11)
at Function.load (src/ComponentLoader.mjs:17:13)
at test/ComponentLoaderB.test.mjs:21:44
The interesting element from my point of view is the fact the ComponentLoaderA.test.mjs and the ComponentLoaderB.test.mjs are exactly the same.
The full error trace I found is:
CComponentLoader load load method is able to load a component from a file
Error:
at invariant (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:2004:11)
at Runtime.loadEsmModule (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:534:7)
at Runtime.linkModules (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:616:19)
at importModuleDynamically (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:555:16)
at importModuleDynamicallyWrapper (internal/vm/module.js:443:21)
at exports.importModuleDynamicallyCallback (internal/process/esm_loader.js:30:14)
at Function.load (importTest/.stryker-tmp/sandbox7042873/src/ComponentLoader.mjs:89:11)
at importTest/.stryker-tmp/sandbox7042873/test/ComponentLoaderB.test.mjs:22:37
at new Promise (<anonymous>)
at Object.<anonymous> (importTest/.stryker-tmp/sandbox7042873/test/ComponentLoaderB.test.mjs:14:79)
It seems the error does not have any message.
Further information from the jest-runtime investigation :
It seems that between the tests, the sandbox context is lost for a reason I cannot be able to manage to find at the moment.
In node_modules/jest-runtime/build/index.js:2004:11 :
The condition is NULL, the error is then thrown without a message.
function invariant(condition, message) {
if (!condition) {
throw new Error(message);
}
}
In node_modules/jest-runtime/build/index.js:534:7 :
The context is NULL, and no message given, creating my empty error message.
const context = this._environment.getVmContext();
invariant(context);
The toString() of this._environment.getVmContext method as follow:
getVmContext() {
return this.context;
}
The current _environment presents a null context :
NodeEnvironment {
context: null,
fakeTimers: null,
[...]
}
The deeper point I can reach is this code where the context appear to be null :
const module = new (_vm().SourceTextModule)(transformedCode, {
context,
identifier: modulePath,
importModuleDynamically: (specifier, referencingModule) => {
return this.linkModules(
specifier,
referencingModule.identifier,
referencingModule.context
)},
initializeImportMeta(meta) {
meta.url = (0, _url().pathToFileURL)(modulePath).href;
}
});
The context variable is not empty and _vm().SourceTextModule is a class extending Module.
I can notice in the importModuleDynamically execution using console.log(this._environment) that the context is currently null.

Resources