Can handle multiple errors using Nestjs' Exception Filter? - nestjs

Spring boot was able to handle various errors using #RestControllerAdvice. The code created by spring boot is as follows.
#RestControllerAdvice
public class ControllerExceptionHandler {
// #valid에서 바인딩 에러가 발생
#ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException ==> " + e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.WRONG_INPUT_VALUE, e.getBindingResult());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
// 사용자 생성시 이메일이 중복된 경우
#ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicateEmailException(DuplicateEmailException e) {
log.error("handleDuplicateEmailException ==> " + e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.DUPLICATE_EMAIL_VALUE);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
...
}
Currently, I was looking for features similar to #RestControllerAdvice while using NestJs to handle errors, and I found out about ExceptionFilter. I wrote the code, but I had a question after writing it. If we process the error like this, we can only process errors related to HttPexception, right?
I want to handle other errors globally besides HttPexception. However, unlike Spring Boot's #RestControllerAdvice, ExceptionFilter does not seem to be able to handle many errors in a single class. Am I using it wrong?
#Catch()
export class ExceptionHandler implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
path: request.url
});
}
}

You have to pass exception class to #Catch() decorator.
If you want to catch all http exceptions you can do
import { ExceptionFilter, Catch } from '#nestjs/common';
#Catch(HttpException)
export class ExceptionHandler implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
path: request.url
});
}
}
This will catch all errors thrown when executing any route.
You can also pass a few exception classes to catch only particular exceptions.
#Catch(BadRequestException, UnauthorizedException)
You can read more at https://docs.nestjs.com/exception-filters

Related

Exception Interceptor should modify res

I implemented an exception interceptor to intercept my HttpExceptions and return a http response depending on which HttpException is thrown by my application.
In my case I am throwing a 403 http exception in another interceptor which used by a route and declare the exception interceptor as a global one.
My interceptor looks like:
#Injectable()
export class ExceptionInterceptor implements NestInterceptor<Response> {
constructor(
private readonly logger: LoggerService
) {
this.logger.setContext(this.constructor.name);
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const url = getOriginalUrl(request);
return next.handle().pipe(
catchError(
exception => {
switch (exception.status) {
case HttpStatus.FORBIDDEN: {
const forbiddenResBody = getForbiddenResBody(exception.response, url);
response.status(HttpStatus.FORBIDDEN);
response.header('Content-type', 'application/problem+json');
this.logger.error(
'Unauthorized',
exception
)
return of(forbiddenResBody)
}
default: {
const invalidServerResBody = getInternalServerResBody(url);
console.log('it is default')
this.logger.error(
'Unexpected exception',
exception
)
return of(invalidServerResBody);
}
}
})
);
}
}
However, the response is not modified is there anything that I am missing ?
Any help would be really appreciated !

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 prevent file upload when body validation fails in nestjs

I have the multipart form to be validated before file upload in nestjs application. the thing is that I don't want the file to be uploaded if validation of body fails.
here is how I wrote the code for.
// User controller method for create user with upload image
#Post()
#UseInterceptors(FileInterceptor('image'))
create(
#Body() userInput: CreateUserDto,
#UploadedFile(
new ParseFilePipe({
validators: [
// some validator here
]
})
) image: Express.Multer.File,
) {
return this.userService.create({ ...userInput, image: image.path });
}
Tried so many ways to turn around this issue, but didn't reach to any solution
Interceptors run before pipes do, so there's no way to make the saving of the file not happen unless you manage that yourself in your service. However, another option could be a custom exception filter that unlinks the file on error so that you don't have to worry about it post-upload
This is how I created the whole filter
import { isArray } from 'lodash';
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '#nestjs/common';
import { Request, Response } from 'express';
import * as fs from 'fs';
#Catch(BadRequestException)
export class DeleteFileOnErrorFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const getFiles = (files: Express.Multer.File[] | unknown | undefined) => {
if (!files) return [];
if (isArray(files)) return files;
return Object.values(files);
};
const filePaths = getFiles(request.files);
for (const file of filePaths) {
fs.unlink(file.path, (err) => {
if (err) {
console.error(err);
return err;
}
});
}
response.status(status).json(exception.getResponse());
}
}

Problem trying to show error message with prism and node

I'm doing the backend of a ToDo's project, and for that I'm using the prism orm, the node framework, the typescript language and to test the project I'm using insomnia. i already did all the methods i wanted (create ToDo, get ToDo's, delete ToDo, change ToDo task, change ToDo title) and they are all working, but when i went to test the error message that would occur when i tried to create a ToDo that already exists, in insomia it gives the following message:
Error: Server returned nothing (no headers, no data)
And in VsCode:
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "#<AppError>".] {
code: 'ERR_UNHANDLED_REJECTION'
}
I made the code as follows:
export class AppError {
public readonly message: string;
public readonly statusCode: number;
constructor(message: string, statusCode = 400) {
this.message = message
this.statusCode = statusCode
}
}
import { AppError } from "../../errors/AppError";
import { prisma } from "../../client/prismaClient";
import { CreateToDoType } from "../Types/CreateToDoType";
export class createToDoUseCase {
async execute({title, task}: CreateToDoType) {
const todoAlreadyExists = await prisma.toDo.findUnique({
where: {
title: title
}
})
if (!todoAlreadyExists) {
const newToDo = await prisma.toDo.create({
data: {
title: title,
task: task
}
})
return newToDo
} else {
console.log("01 4")
throw new AppError("Error! ToDo already exists") //aqui está o erro
}
}
}
import {Request, Response} from "express"
import { createToDoUseCase } from "../UseCases/CreateToDoUseCases"
export class createToDoController {
async handle(req: Request, res: Response) {
const { title, task } = req.body
const CreateToDoUseCases = new createToDoUseCase()
const result = await CreateToDoUseCases.execute({title, task}) // aqui está o erro
return res.status(201).json(result)
}
}

Nestjs Interceptor how to catch http 401 error and resubmit original request

I need to write an http header interceptor to add Authorization header, if there is a 401 error, submit another request for a new token, then resubmit the original request with the new token.
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
catchError(async error => {
if (error.response.status === 401) {
const originalRequest = error.config;
var authRes = await this.authenticationService.getAccessToken();
this.authenticationService.accessTokenSubject.next(authRes.access_token);
// I need to resubmit the original request with the new token from here
// but return next.handle(originalRequest) doesn't work
}
return throwError(error);
}),
);
}
But next.handle(originalRequest) doesn't work. How to resubmit the original request in the interceptor? Thank you very much in advance for your help.
I just encountered a similar problem, where I can catch the exception from exception filter but can't do so in interception layer.
So I looked up the manual and found it says:
Any exception thrown by a guard will be handled by the exceptions layer
(global exceptions filter and any exceptions filters that are applied to the current context).
So, if the exception is thrown from AuthGuard context(including the validate method in your AuthService), probably better to move the additional logic by extending the Authguard
like this:
export class CustomizedAuthGuard extends AuthGuard('strategy') {
handleRequest(err, user, info, context, status) {
if (err || !user) {
// your logic here
throw err || new UnauthorizedException();
}
return user;
}
}
or simply using customized exception filter.
It's been a while since the question but maybe it will help someone.
Ok, suppose that we need handle unauthorize exception out of route and guards, maybe service to service. So you can implement a interceptor like that and add some logic to get some data if needed, Ex: inject some Service in the interceptor.
So, throw an unauthorize exception and we are going to intercept it:
#Injectable()
export class UnauthorizedInterceptor implements NestInterceptor {
constructor(
private readonly authService: AuthService,
private readonly httpService: HttpService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
const {
response: { status, config },
} = err;
// assuming we have a request body
const jsonData = JSON.parse(config.data);
if (status === HttpStatus.UNAUTHORIZED) {
// We can use some data in payload to find user data
// here for example the user email
if (jsonData?.email) {
return
from(this.authService.getByUserEmail(jsonData.email)).pipe(
switchMap((user: User) => {
if (user) {
// Ex: we can have stored token info in user entity.
// call function to refresh access token and update user data
// with new tokens
return from(this.authService.refreshToken(user)).pipe(
switchMap((updatedUser: User) => {
// now updatedUser have the new accessToken
const { accessToken } = updatedUser;
// set the new token to config (original request)
config.headers['Authorization'] = `Bearer ${accessToken}`;
// and use the underlying Axios instance created by #nestjs/axios
// to resubmit the original request
return of(this.httpService.axiosRef(config));
}),
);
}
}),
);
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
}),
);
}
}

Resources