0. 개발환경
nest : 10.2.1
class-validator: 0.14.0
* 참고 : class-validator - npm (npmjs.com)
1. 문제의 발생
// user.entity.ts
@Entity('user')
export class User extends CoreEntity {
...
@ApiProperty({ example: '홍길동' })
@Column({ type: 'varchar', nullable: true })
@Length(2, 10)
@IsString()
public username?: string;
...
}
class-validator 라이브러리를 사용해서 'username' 필드의 Length를 제한하고, 예외의 경우를 넣어서 출력되는 메시지를 확인해보았습니다.
BadRequestException: Bad Request Exception
at ValidationPipe.exceptionFactory (/workspaces/nestjs-personal-backend/node_modules/@nestjs/common/pipes/validation.pipe.js:101:20)
at ValidationPipe.transform (/workspaces/nestjs-personal-backend/node_modules/@nestjs/common/pipes/validation.pipe.js:74:30)
...
{
response: {
message: [ 'username must be longer than or equal to 2 characters' ],
error: 'Bad Request',
statusCode: 400
},
status: 400,
options: {}
}
나는 error message가 string으로 나오는 경우만 고려해서 filter를 짜뒀기에 상세 message인 'username must be longer than or equal to 2 characters' 가 error response 상에 출력되지 않는 문제가 발생했습니다.
2. 구조분석 및 원인 파악
export interface ValidatorPackage {
validate(object: unknown, validatorOptions?: ValidatorOptions): ValidationError[] | Promise<ValidationError[]>;
}
ValidationError의 경우 항상 array의 형태로 반환되고, class-validator 라이브러리 내에서는 Array[ Object ] 의 형태로 error를 throw하고 있습니다. 아무래도 한 request에 대해서 여러 validation error가 동시에 발생할 수 있기 때문인듯합니다.
4. 문제의 해결
나는 error format의 변경이 필요하다. 이후 Validation Error를 위한 Exception을 만들어 줄 것입니다.
기본적으로 ValidationException 상황은 값이 잘못들어온 BadRequest 상황임으로 extends해줍니다.
// common/exceptions/validation.exception.ts
import { BadRequestException, ValidationError } from '@nestjs/common';
export class ValidationException extends BadRequestException {
constructor(public errors: ValidationError[]) {
super();
}
override message: string = 'Validation Exception';
}
이후 ValidationPipe를 추가할 때, exceptionFactory 옵션을 추가해주고, 만들어준 ValidationException을 throw하도록 합니다.
// main.ts
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (errors) => {
const result = errors.map((error) => ({
[error.property]: error.constraints,
}));
return new ValidationException(result);
},
transform: true,
}),
);
그리고 global하게 exception을 처리하는 filter에도 이를 반영해줍니다.
아래는 모든 exception을 처리해주는 filter인데, NestJS에서 기본적으로 제공하는 ExceptionFilter를 오버라이딩해서 사용하고 있습니다. 이에 관해서는 공식문서에서 자세히 언급하고 있으니 참고만 해주시면 될 것 같습니다.
* 참고 : https://docs.nestjs.com/exception-filters
// /common/filters/all-exception.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger: Logger = new Logger();
public catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const res = ctx.getResponse<Response>();
const req = ctx.getRequest<Request>();
const statusCode = getHttpStatus(exception);
const message = getMessage(exception);
const detail = getDetail(exception);
const exceptionResponse: ExceptionResponse = {
statusCode,
timestamp: new Date(),
path: req.url,
method: req.method,
message,
detail,
};
const args = req.body;
this.exceptionLogging(exceptionResponse, args);
res.status(statusCode).json(exceptionResponse);
}
...
}
아래와 같이 다른 exception들과 format 통일을 위해서 instanceof로 타입 추론을 하면서 exception 분류를 할 수 있습니다.
방금 추가한 ValidationException에 대해서도 각 errors의 value를 key: value로 하는 object를 만들어서, 발생하는 validation exception을 모아서 detail로 알려주게 됩니다.
// /common/filters/utils
export function getDetail(exception: unknown): string | Array<object> | object {
let detail: string | Array<object> | object = '';
if (exception instanceof HttpException) {
detail = exception.getResponse;
}
if (exception instanceof QueryFailedError) {
detail = { query: exception.query, parameters: exception.parameters };
}
if (exception instanceof ValidationException) {
detail = exception.errors.map((error) => ({
[error.property]: error.constraints,
}));
}
return detail;
}
5. 참고문서
https://github.com/typestack/class-validator/issues/2055#issuecomment-1521392106
How to customize validation messages globally? · Issue #169 · typestack/class-validator (github.com)
How to format error message response? · Issue #707 · typestack/class-validator (github.com)