NestJS - ValidationPipe in WebsocketGateway returns Internal Server Error

Viewed 3254

I'm trying to add some validation to my WebSocketGateway in NestJS. Here's the code:

// MessageDTO

import { IsNotEmpty, MinLength } from 'class-validator';

export class MessageDTO {
  @IsNotEmpty()
  username: string;

  @IsNotEmpty()
  @MinLength(10)
  text: string;
}
// Gateway

import { ValidationPipe, UsePipes } from '@nestjs/common';
import { MessageBody, SubscribeMessage, WebSocketGateway, WsResponse } from '@nestjs/websockets';
import { MessageService } from './message/message.service';
import { MessageDTO } from './message/message.dto';
import { Message } from './message/message.entity';

@WebSocketGateway()
export class AppGateway {
  constructor(private readonly messageService: MessageService) {}

  @UsePipes(new ValidationPipe())
  @SubscribeMessage('message')
  async handleMessage(@MessageBody() dto: MessageDTO): Promise<WsResponse<Message>> {
    const message = await this.messageService.saveMessage(dto);
    return { event: 'message', data: message };
  }
}

Now, when I try to send a message that doesn't meet validation rules, it errors out, but the client always receives { status: 'error', message: 'Internal server error'}. Also, Nest logs the error to the console (which I belive should not happen...?):

thing_api | Error: Bad Request Exception
thing_api |     at ValidationPipe.exceptionFactory (/usr/src/app/node_modules/@nestjs/common/pipes/validation.pipe.js:78:20)
thing_api |     at ValidationPipe.transform (/usr/src/app/node_modules/@nestjs/common/pipes/validation.pipe.js:50:24)
thing_api |     at processTicksAndRejections (internal/process/task_queues.js:89:5)
thing_api |     at async resolveParamValue (/usr/src/app/node_modules/@nestjs/websockets/context/ws-context-creator.js:104:31)
thing_api |     at async Promise.all (index 0)
thing_api |     at async pipesFn (/usr/src/app/node_modules/@nestjs/websockets/context/ws-context-creator.js:106:13)
thing_api |     at async /usr/src/app/node_modules/@nestjs/websockets/context/ws-context-creator.js:41:17
thing_api |     at async AppGateway.<anonymous> (/usr/src/app/node_modules/@nestjs/websockets/context/ws-proxy.js:11:32)
thing_api |     at async WebSocketsController.pickResult (/usr/src/app/node_modules/@nestjs/websockets/web-sockets-controller.js:85:24)

However, if I use the same DTO and validation pipe in regular controller, it works like a charm - with malformed payload I get properly formatted error message. Can somebody point out what I'm doing wrong?

4 Answers

You can rewrite the default websocket filter, to catch both http exception and websocket exception.

import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch(WsException, HttpException)
export class WsExceptionFilter {
  public catch(exception: HttpException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    this.handleError(client, exception);
  }

  public handleError(client: Socket, exception: HttpException | WsException) {
    if (exception instanceof HttpException) {
      // handle http exception
    } else {
      // handle websocket exception
    }
  }
}

Then use it in your gateway

@UseFilters(WsExceptionFilter)
@WebSocketGateway()
export class WorkspacesGateway {}

BadRequestException is a child class of HttpException. Nest's default exception handler for websockets checks to see if the caught exception is an instanceof WsException and if not returns an unknown exception.

To get around this, you can implement a filter that catches BadRequestException and transforms it to an appropriate WsException before having Nest's exception filter handle the exception from there.

@Catch(BadRequestException)
export class BadRequestTransformationFilter extends BaseWsExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentHost) {
    const properException = new WsException(exception.getResponse());
    super.catch(properException, host);
  }
}

I have create my SocketValidation Pipe

import { PipeTransform, Injectable, ArgumentMetadata, ValidationPipe } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { WsException } from '@nestjs/websockets';

@Injectable()
export class SocketValidationPipe  implements PipeTransform<any> {

  constructor() {
    // super(options)
  }

  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, JSON.parse(value));
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new WsException('Wrong message!');//new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

ValidationPipe got exceptionFactory option so you can just pass a custom factory to it, like:

@UsePipes(new ValidationPipe({
    exceptionFactory(validationErrors = []) {
        if (this.isDetailedOutputDisabled) {
            return new WsException('Bad request');
        }
        const errors = this.flattenValidationErrors(validationErrors);

        return new WsException(errors);
    }
}))

Or extend built-in ValidationPipe

import {Injectable, ValidationPipe} from '@nestjs/common';
import {WsException} from "@nestjs/websockets";

@Injectable()
export class WSValidationPipe extends ValidationPipe{

  createExceptionFactory() {
    return (validationErrors = []) => {
      if (this.isDetailedOutputDisabled) {
        return new WsException('Bad request');
      }
      const errors = this.flattenValidationErrors(validationErrors);

      return new WsException(errors);
    };
  }

}
Related