How to make class-validator to stop on error? - nestjs

I want class validator to stop validation as soon as the first error is found.
I know there is a stopAtFirstError option, but that prevents further validation on property level only. I want to stop it globally:
#IsString() // Let's say this received invalid value
someStr: string
#IsInt() // I want this to be NOT executed
someNuber: number
Is this possible?

Since there is no official support for this, I implemented a hacky way to make it work. Although it doesn't stop the validation, only returns the message from initial one & ignores the rest.
ValidationErrorException
// validation-error.exception.ts
import { HttpException, HttpStatus } from "#nestjs/common";
export class ValidationErrorException extends HttpException {
data: any;
constructor(data: any) {
super('ValidationError', HttpStatus.BAD_REQUEST);
this.data = data
}
getData(){
return this.data;
}
}
Use Exception Factory to return the exception
// main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
exceptionFactory: (errors: ValidationError[]) => {
return new ValidationErrorException(errors);
},
}),
);
Validation Error Filter
// validation-error.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost } from '#nestjs/common';
import { Response } from 'express';
import { ValidationErrorException } from './validation-error.exception';
#Catch(ValidationErrorException)
export class ValidationErrorFilter implements ExceptionFilter {
catch(exception: ValidationErrorException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
const status = exception.getStatus();
let constraints = exception.getData()[0].constraints
response
.status(status)
.json({
statusCode: status,
message: constraints[Object.keys(constraints)[0]],
error: exception.message
});
}
}
Use The filter
// app.module.ts
providers: [
{
provide: APP_FILTER,
useClass: ValidationErrorFilter,
},
],

Related

In nestjs, how can we change default error messages from typeORM globally?

I have this code to change the default message from typeorm when a value in a unique column already exists. It just creates a custom message when we get an error 23505.
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
I will have to use it in other services, so I would like to abstract that code.
I think I could just create a helper and then I import and call it wherever I need it. But I don’t know if there is a better solution to use it globally with a filter or an interceptor, so I don’t have to even import and call it in different services.
Is this possible? how can that be done?
If it is not possible, what do you think the best solution would be?
Here all the service code:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
try {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
} catch (error) {
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
}
}
public async findOneByEmail(email: string): Promise<Merchant | null> {
return this.merchantRepository.findOneBy({ email });
}
}
I created an exception filter for typeORM errors.
This was the result:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
InternalServerErrorException,
} from '#nestjs/common';
import { Response } from 'express';
import { QueryFailedError, TypeORMError } from 'typeorm';
type ExceptionResponse = {
statusCode: number;
message: string;
};
#Catch(TypeORMError, QueryFailedError)
export class TypeORMExceptionFilter implements ExceptionFilter {
private defaultExceptionResponse: ExceptionResponse =
new InternalServerErrorException().getResponse() as ExceptionResponse;
private exceptionResponse: ExceptionResponse = this.defaultExceptionResponse;
catch(exception: TypeORMError | QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
exception instanceof QueryFailedError &&
this.setQueryFailedErrorResponse(exception);
response
.status(this.exceptionResponse.statusCode)
.json(this.exceptionResponse);
}
private setQueryFailedErrorResponse(exception: QueryFailedError): void {
const error = exception.driverError;
if (error.code === '23505') {
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
this.exceptionResponse = {
statusCode: HttpStatus.BAD_REQUEST,
message,
};
}
// Other error codes can be handled here
}
// Add more methods here to set a different response for any other typeORM error, if needed.
// All typeORM erros: https://github.com/typeorm/typeorm/tree/master/src/error
}
I set it globally:
import { TypeORMExceptionFilter } from './common';
async function bootstrap() {
//...Other code
app.useGlobalFilters(new TypeORMExceptionFilter());
//...Other code
await app.listen(3000);
}
bootstrap();
And now I don't have to add any code when doing changes in the database:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
}
}
Notice that now I don't use try catch because nest is handling the exceptions. When the repository save() method returns an error (actually it is a rejected promise), it is caught in the filter.

How to send raw data as payload with MQTT on Nestjs

I want to send just raw data to the broker but the string is sent as string with quotations.
i.e.
the message I want to sent is: ti=0F:0000000000&id=E8EB1BE99345
but what is sent is: "ti=0F:0000000000&id=E8EB1BE99345"
How can I achieve that?
I have declared serializer in the mqtt client like below,
#Module({
imports: [
ClientsModule.register([
{
name: 'MQTT_CLIENT',
transport: Transport.MQTT,
options: {
url: 'mqtt://XX.XXX.XXX.XXX:1883',
clientId: 'my-client-id',
serializer: {
serialize: (value: any) => value.data,
},
},
},
]),
ConfigModule.forRoot(),
],
controllers: [AppController],
})
export class AppModule {}
You can see here that the ACKs are sent with the quotations, and they should be sent like above, without them:
Apparently this behaviour is something standard in Nestjs, the publishing response will be returned as the result object of applying JSON.stringify().
There's no way of configure it anyhow.
The only option for doing that is to create a custom ClientProxy or even easier extend from ClientMqtt and then override the publish method, so all the logic related with serialization is ignored, and just publish the raw data of the packet.
Custom class with the modified publish method:
import { ClientMqtt, ReadPacket, WritePacket } from '#nestjs/microservices';
export class RawPayloadProxy extends ClientMqtt {
protected publish(
partialPacket: ReadPacket,
callback: (packet: WritePacket) => any,
): () => void {
try {
const pattern = this.normalizePattern(partialPacket.pattern);
const responseChannel = this.getResponsePattern(pattern);
let subscriptionsCount =
this.subscriptionsCount.get(responseChannel) || 0;
const publishPacket = () => {
subscriptionsCount = this.subscriptionsCount.get(responseChannel) || 0;
this.subscriptionsCount.set(responseChannel, subscriptionsCount + 1);
this.mqttClient.publish(
this.getRequestPattern(pattern),
partialPacket.data,
);
};
if (subscriptionsCount <= 0) {
this.mqttClient.subscribe(
responseChannel,
(err: any) => !err && publishPacket(),
);
} else {
publishPacket();
}
return () => {
this.unsubscribeFromChannel(responseChannel);
};
} catch (err) {
callback({ err });
}
}
}
Definition of the custom client class on AppModule:
#Module({
imports: [
ClientsModule.register([
{
name: 'MQTT_CLIENT',
customClass: RawPayloadProxy,
options: {
url: 'mqtt://XX.XXX.XXX.XXX:1883',
clientId: 'client-xxx',
},
},
]),
],
controllers: [AppController],
})
export class AppModule {}

NestJS Exception filter messes up error array if it comes from ValidationPipe

So I use the ValidationPipe to validate my DTOs in NestJS, like this:
// auth.dto.ts
export class AuthDto {
#IsEmail()
#IsNotEmpty()
email: string;
}
Without the Exception filter the error message works as intended. I leave the email field empty and I receive an array of error messages:
// Response - Message array, but no wrapper
{
"statusCode": 400,
"message": [
"email should not be empty",
"email must be an email"
],
"error": "Bad Request"
}
Perfect. Now I want to implement a wrapper for the error messages, so I create a new filter and add it to to bootstrap:
// main.ts
async function bootstrap() {
// ...
app.useGlobalFilters(new GlobalExceptionFilter());
}
bootstrap();
// global-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '#nestjs/common';
import { Response } from 'express';
import { IncomingMessage } from 'http';
export const getStatusCode = <T>(exception: T): number => {
return exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
};
export const getErrorMessage = <T>(exception: T): string => {
return exception instanceof HttpException
? exception.message
: String(exception);
};
#Catch()
export class GlobalExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<IncomingMessage>();
const statusCode = getStatusCode<T>(exception);
const message = getErrorMessage<T>(exception);
response.status(statusCode).json({
error: {
timestamp: new Date().toISOString(),
path: request.url,
statusCode,
message,
},
});
}
}
It works great for most of my errors:
// Response - Good format (wrapped), single message expected
{
"error": {
"timestamp": "2022-05-11T19:54:59.093Z",
"path": "/auth/signup",
"statusCode": 400,
"message": "Email already in use"
}
}
But when I get a ValidationError from the ValidationPipe it should give me an array or messages like before, but it gives this message instead:
// Response - Wrapper: check, single message instead of array
{
"error": {
"timestamp": "2022-05-11T19:59:17.282Z",
"path": "/auth/signup",
"statusCode": 400,
"message": "Bad Request Exception" // it should be "message": ["not empty", "must be email"]
}
}
The exception object in my exception filter has a response field which contains the message array:
// HttpException object inside the filter class
{
response: {
statusCode: 400,
message: [ 'email should not be empty', 'email must be an email' ],
error: 'Bad Request'
},
status: 400
}
But exception.response.message doesn't work, because the field is private and TypeScript throws an error:Property 'response' is private and only accessible within class 'HttpException'.
Does any of you know how could I reach the message array, so I could format my error response properly?
EDIT: Sorry for the long post!
As #tobias-s commented, there's a workaround, which solved the problem:
Try exception["response"]["message"]. This bypasses the private restriction
As you are using #Catch decorator, you could get a HttpException or not, so we need to evaluate it.
Let's create an interface to "parse" Nest.js built-in HttpException class response:
export interface HttpExceptionResponse {
statusCode: number;
message: any;
error: string;
}
Now we can process it:
export const getErrorMessage = <T>(exception: T): any => {
if(exception instanceof HttpException) {
const errorResponse = exception.getResponse();
const errorMessage = (errorResponse as HttpExceptionResponse).message || exception.message;
return errorMessage;
} else {
return String(exception);
}
};
exception.getResponse() can be a string or an object, that's because we handle it as message: any of course.

Class-Validator node.js provide custom error

I have a custom validator constraint and annotation created for checking whether entity with given property already exists or not, here is the code
import { Inject, Injectable } from '#nestjs/common';
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint } from 'class-validator';
import { ValidatorConstraintInterface } from 'class-validator/types/validation/ValidatorConstraintInterface';
import { Connection } from 'typeorm';
import { InjectConnection } from '#nestjs/typeorm';
#ValidatorConstraint({ async: true })
#Injectable()
export class EntityExistsConstraint implements ValidatorConstraintInterface {
constructor(#InjectConnection() private dbConnection: Connection) {
}
defaultMessage(validationArguments?: ValidationArguments): string {
return `${validationArguments.constraints[0].name} with ${validationArguments.property} already exists`;
}
validate(value: any, validationArguments?: ValidationArguments): Promise<boolean> | boolean {
const repoName = validationArguments.constraints[0];
const property = validationArguments.property;
const repository = this.dbConnection.getRepository(repoName);
return repository.findOne({ [property]: value }).then(result => {
return !result;
});
}
}
export function EntityExists(repoName, validationOptions?: ValidationOptions) {
return function(object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [repoName],
validator: EntityExistsConstraint,
});
};
}
Everything works fine, but I receive this response when the validation fails
{
"statusCode": 400,
"message": [
"User with email already exists"
],
"error": "Bad Request"
}
I want the error be Conflict Exception=> statusCode 409, how can I achieve this?
class-validator doesn't do anything with the http codes. It only validates and returns a list of errors or an empty array.
What you need to do is to check framework you use, I assume it's nestjs or routing-controllers.
In the case of routing-controllers you need to write own after middleware and disable default middleware (it converts validation errors to 400 bad requests).
More info is here: https://github.com/typestack/routing-controllers#error-handlers
In the case of nestjs - the same steps.
More info you can find here: https://docs.nestjs.com/exception-filters#catch-everything

NestJS How to add custom Logger to custom ExceptionFilter

I am using NestJS 5.4.0
I have custom LoggerService, it's working perfectly. But, how can I add this LoggerService to ExceptionFilter.
// logger.service.ts
import {Injectable, LoggerService} from '#nestjs/common';
#Injectable()
export class Logger implements LoggerService {
log(message: string) {
console.log(message);
}
error(message: string, trace: string) {
console.error(message);
}
warn(message: string) {
console.warn(message);
}
}
//logger.module.ts
import { Module } from '#nestjs/common';
import {Logger} from '../services/logger.service';
#Module({
providers: [Logger],
exports: [Logger],
})
export class LoggerModule {}
// user.module.ts
import { Module } from '#nestjs/common';
import {UserService} from '../services/user.service';
import {LoggerModule} from './logger.module';
#Module({
imports: [LoggerModule],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
It's working perfectly.
import {Logger} from './logger.service';
export class UserService {
constructor(
private logger: Logger
) {}
private test = () => {
this.logger.log("test"); // log success "test" to console
}
}
But how can I add my custom Logger to ExceptionFilter
// forbidden.exception.filter.ts
import {HttpException, HttpStatus, Injectable} from '#nestjs/common';
#Injectable()
export class ForbiddenException extends HttpException {
constructor(message?: string) {
super(message || 'Forbidden', HttpStatus.FORBIDDEN);
// I want to add my custom logger here!
}
}
Thank for reading.
First of all your class ForbiddenException extends HttpException is not
what it calls ExceptionFilter. ExceptionFilter is
exceptions layer which is responsible for processing all unhandled exceptions across an application
docs
You provided exmaple when you are trying to inject it to your custom HttpException. But thats wrong. Your exception don't have to be responsible for logging. Thats what ExceptionFilter should be responsible for.
Anyway, for now (17 oct 2019) there is no example in official docs how to inject providers to ExceptionFilter.
You can pass it to constructor on init, but you should to get Logger instance before with app.get<T>(...) method.
For example I've changed code from exception-filters docs:
// HttpExceptionFilter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '#nestjs/common';
import { Request, Response } from 'express';
import {MyLogger} from '../MyLogger'
#Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: MyLogger) {}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
if (status >= 500) {
this.logger.error({ request, response });
}
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
and bootstrap.ts code:
// bootstrap.ts
const app = await NestFactory.create(MainModule, {
logger: false,
});
const logger = app.get<MyLogger>(MyLogger);
app.useLogger(logger);
app.useGlobalFilters(new HttpExceptionFilter(logger));
This technique can be used for all this INestApplication methods:
app.useGlobalFilters
app.useGlobalGuards
app.useGlobalInterceptors
app.useGlobalPipes
app.useLogger
app.useWebSocketAdapter
First of all, to use dependency injection with Exception filters you cannot register them using the useGlobalFilters() method:
const app = await NestFactory.create(MainModule, {
logger: false,
});
const logger = app.get<MyLogger>(MyLogger);
app.useLogger(logger);
//Remove this line
//app.useGlobalFilters(new HttpExceptionFilter(logger));
Next in your MainModule, add your custom exception filter as a provider (note: filters are automatically set as global no matter what module you add them to but as a best practice, add them to your top level module):
import { Module } from '#nestjs/common';
import { APP_FILTER } from '#nestjs/core';
import { LoggerModule } from './logger.module';
import { ForbiddenException } from './forbidden.exception.filter.ts';
#Module({
imports: [
LoggerModule //this is your logger module
],
providers: [
{
provide: APP_FILTER, //you have to use this custom provider
useClass: ForbiddenException //this is your custom exception filter
}
]
})
export class MainModule {}
Now you can inject the logger into your custom exception filter:
import {HttpException, HttpStatus, Injectable} from '#nestjs/common';
import { Logger } from './path/to/logger';
#Injectable()
export class ForbiddenException extends HttpException {
constructor(private logger: Logger) {}
catch(exception: HttpException, response) {
this.logger.log('test');
}
}
Pseudo code but I think you get the idea.

Resources