Nest: ValidationPipe with custom error

Created on 6 Nov 2018  路  9Comments  路  Source: nestjs/nest

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

A validation error returns a response with status code 400.

Expected behavior

The ValidationPipe is configurable and accepts a function used to create an error instance which will be thrown.

Minimal reproduction of the problem with instructions

async function bootstrap () {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe())
  await app.listen(3000)
}

What is the motivation / use case for changing the behavior?

Per-field validation errors are, in my opinion, a 422, not a 400. I'd also suggest changing this framework-wide as a default, but that might be a breaking change that people might not welcome with open arms.

Environment

"@nestjs/common": "^5.1.0",
"@nestjs/core": "^5.1.0",
"@nestjs/typeorm": "^5.2.2",
common question 馃檶

Most helpful comment

Since 5.5.0 you can override exception's factory:

new ValidationPipe({
  exceptionFactory: (errors: ValidationError[]) => 
    new BadRequestException('Validation error'),
});

All 9 comments

What about (2 ways):
1) extending ValidationPipe and rethrowing different error?
2) creating a filter that transforms BadRequestException (when ValidationError) to an instance expected by your application?

Thanks for the response. Here are my attempts at both approaches for reference.

Extend ValidationPipe

export class ValidationPipe422 extends ValidationPipe {
  public async transform (value, metadata: ArgumentMetadata) {
    try {
      return await super.transform(value, metadata)
    } catch (e) {
      if (e instanceof BadRequestException) {
        throw new UnprocessableEntityException(e.message.message)
      }
    }
  }
}

async function bootstrap () {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe422())
}

A small trick: you _need_ return await here (although usually we can just write return since async makes it return a Promise anyway). Without await, the function simply returns a (pending) promise properly and thus nothing is caught. With await, the function stalls until the promise is rejected, the rejection is caught and the catch block executes.

With an exception filter

I'm not sure how to "properly" do the _(when ValidationError)_ bit from your response. :thinking: I've got the following, which works, but now it will transform every 400 into a 422.

@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter<BadRequestException> {
  public catch (exception, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse() as express.Response
    response
      .status(422)
      .json({
        statusCode: 422,
        error: `Unprocessable Entity`,
        message: exception.message.message,
      })
  }
}

async function bootstrap () {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe())
  app.useGlobalFilters(new ValidationExceptionFilter())
}

One obvious way to fix this is to inspect the expection.message.message and see if it quacks like a "400 but I want a 422" error, but I really don't like that one.

As for my initial "feature request": I propose exceptionConstructor where could just pass in an UnprocessableEntityException, and then the ValidationPipe would user whatever I pass it in:

https://github.com/nestjs/nest/blob/a95b3d6048c9ad828b692b5560194cbc592654e6/packages/common/pipes/validation.pipe.ts#L46-L48

It would default to BadRequest which would make it a non-breaking change.

Then usage would be just this:

async function bootstrap () {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    exceptionConstructor: UnprocessableEntityException,
  }))
  await app.listen(3000)
}

Since 5.5.0 you can override exception's factory:

new ValidationPipe({
  exceptionFactory: (errors: ValidationError[]) => 
    new BadRequestException('Validation error'),
});

Since 5.5.0 you can override exception's factory:

new ValidationPipe({
  exceptionFactory: (errors: ValidationError[]) => 
    new BadRequestException('Validation error'),
});

There's one tiny bit missing that drove me mad... this code would end in throwing "undefined".
Which would lead to an error which can be debugged hardly.
Make sure to use

exceptionFactory: (errors: ValidationError[]) => RETURN new BadRequestException('Validation error'),

... then everything will be fine :)

@jbjhjm It shouldn't, unless you added { after =>.

@jbjhjm It shouldn't, unless you added { after =>.

Sometimes I feel stupid. Of course. Didn't notice the brackets were skipped. Thanks for clearing up!

What about passing isDetailedOutputDisabled flag into exceptionFactory, otherwise it's unclear to call this.isDetailedOutputDisabled in the custom exceptionFactory, we do know nothing about context without digging into the source code.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings