Pipes
Validate what a socket sends: the ValidationPipe on a gateway, refusals turned into WsException so the client learns what was wrong, and a parameter pipe whose ordinary BadRequestException would reach the client only as Internal server error.
What you'll learn
- Validate message payloads with a ValidationPipe bound to a gateway with @UsePipes()
- Explain why a pipe's BadRequestException reaches a socket as Internal server error, and fix it with an exceptionFactory returning a WsException
- Bind a pipe to one parameter, @MessageBody('id', pipe), and give it its own exceptionFactory
A socket's payload is as untrusted as a request body, and arrives with less ceremony: no content type, no route that already narrowed it, just an event name and whatever JSON the client chose to send. A cat named M with an age of -1 is as wrong over a socket as over HTTP, and the same tools check it: the DTO with class-validator's rules, and the ValidationPipe. What changes is how a refusal reaches the client, and that difference catches nearly everyone the first time.
The same pipes, a different refusal
Pipes run on a gateway's handler parameters exactly as on a controller's: @UsePipes() on the gateway class or a handler, app.useGlobalPipes() for the whole app, or a pipe given to one parameter, @MessageBody('id', ParseIntPipe). A ValidationPipe validates a @MessageBody() typed with a DTO, and transforms it if asked.
But a pipe refuses by throwing, and the pipes Nest ships throw BadRequestException, an HTTP exception. The previous lesson showed what a gateway does with that: the client receives Internal server error, and the list of messages that would have told it what was wrong stays on the server.
{ "status": "error", "message": "Internal server error", "cause": { "pattern": "kennels:book", "data": { "nights": -2 } } }
exceptionFactory
Every built-in pipe takes an exceptionFactory option: a function from the pipe's error to the exception it throws. For a gateway, return a WsException. The ValidationPipe gives it the list of ValidationErrors, each with the failed rules' messages in constraints:
@UsePipes(
new ValidationPipe({
exceptionFactory: (errors) =>
new WsException(errors.map((error) => ({ field: error.property, problems: Object.values(error.constraints ?? {}) }))),
}),
)
@WebSocketGateway()
export class KennelsGateway {}
A WsException with an object or an array is sent exactly as it is, so the shape is yours: this one sends [{ "field": "nights", "problems": ["nights must not be less than 1"] }]. A parameter pipe takes the same option, and its error is the message it would have put in the BadRequestException:
@SubscribeMessage('kennels:book')
book(@MessageBody('nights', new ParseIntPipe({ exceptionFactory: (error) => new WsException(error) })) nights: number) {}
The docs' own example passes the errors straight to new WsException(errors). That works, but sends class-validator's whole ValidationError objects, target object and all, which is more than a client needs and sometimes more than it should see.
Where pipes run
Pipes run per handler parameter, after guards and before the handler, as over HTTP. A gateway-level @UsePipes() applies to every parameter of every handler in it. For a @MessageBody('id') of type number, the ValidationPipe has no class to validate and passes the value on unchanged: a string "2" stays a string, which is why a parameter that must be a number gets ParseIntPipe, which turns "2" into 2 and refuses "abc".
Your task
CreateCatDto carries the rules: a name of at least two characters, an age that is a whole number, not negative.
- Validate every message's payload against its DTO, for the whole gateway. A refusal reaches the client as a
WsExceptioncarrying the list of messages:["name must be longer than or equal to 2 characters", "age must not be less than 0"]. cats:findrefuses an id that is not a number withParseIntPipe's own message,Validation failed (numeric string is expected), as aWsException; a numeric string such as"2"finds Luna.
Before step 2, emit cats:find with { "id": "abc" } in the Socket mode and read what arrives.
When it fails
"message":"Internal server error"for an invalid cat or id: the pipe has noexceptionFactory, or its factory returns an HTTP exception; only aWsExceptionreaches the client with its content.acknowledged with {"id":3,"name":"M","age":-1}: nothing validated the payload. TheValidationPipeis not bound, or the handler's parameter is typed with an interface, which leaves the pipe nothing to validate against.ada received [{"event":"exception","data":[{"target":{…},"property":"name",…}]}]: the factory passed theValidationErrorobjects themselves; send theirconstraintsmessages.Validation failed (numeric string is expected)for everycats:add:ParseIntPipewas bound to the whole gateway with@UsePipes(), so it also received the cat object. Give it to the one parameter.
Remember
- Pipes work on gateways as on controllers: per parameter, after guards.
- Nest's pipes throw
BadRequestException, which a socket client sees asInternal server error. exceptionFactoryreturning aWsExceptionmakes the refusal reach the client with its messages.- A
WsExceptionwith an array or object sends that exact shape; choose it for the client.
Stuck? Show a hint
@UsePipes(new ValidationPipe({ exceptionFactory: (errors) => new WsException(errors.flatMap((e) => Object.values(e.constraints ?? {}))) })) on the gateway class. find: @MessageBody('id', new ParseIntPipe({ exceptionFactory: (error) => new WsException(error) })).