Skip to content

Pipes ​

A gateway's pipes work the same as in HTTP: they receive an argument's value as a serde_json::Value, validate or transform it, and return the result, which is then deserialized into the parameter's type. The difference is in which arguments they process: in a gateway there is only one with input data, the frame's data.

Where they are declared ​

LevelHowApplies to
Gateway#[use_pipes(...)] below #[websocket_gateway]Every #[message_body] of all handlers
Handler#[use_pipes(...)] on top of the methodEvery #[message_body] of that handler
Parameter#[message_body("prop", MyPipe)]That argument only

The order is: the gateway's pipes, then the handler's, and finally the parameter's. Pipes are given by type (ParseIntPipe), and then they are instantiated with dependency injection in the context of the gateway's module, or as an expression (ZodValidationPipe::auto()).

Global pipes (app.use_global_pipes, Provider::app_pipe) are not applied to gateways.

Validating messages ​

Tertulia validates all chat messages with corvis::z DTOs declared with zod_dto! (src/chat/dto.rs):

rust
use corvis::prelude::*;
use corvis::zod_dto;
use serde::Deserialize;

pub fn room_name() -> z::ZString {
    z::string().min(1).max(24).regex("^[a-z0-9][a-z0-9-]*$")
}

#[derive(Debug, Deserialize)]
pub struct SendMessageDto {
    pub room: String,
    pub text: String,
}

zod_dto!(
    SendMessageDto,
    z::strict_object! {
        room: room_name(),
        text: z::string().trim().min(1).max(500),
    }
);

The gateway enables validation for all its handlers:

rust
#[websocket_gateway(path = "/chat")]
#[use_pipes(ZodValidationPipe::auto())]
pub struct ChatGateway { /* ... */ }

ZodValidationPipe::auto() validates every argument whose type has a schema (zod_dto!) and lets the others through. Since the pipe returns the schema's output, the handler receives the text already trimmed by trim().

When validation fails, the client receives an exception frame with the zod issues:

json
{
  "event": "exception",
  "data": {
    "status": "error",
    "message": "Validation failed",
    "statusCode": 400,
    "errors": [{ "code": "too_small", "path": ["text"], "message": "..." }]
  }
}

ValidationPipe (DTOs with #[derive(Validate)]) also works in gateways; see Validation.

One property with its own pipe ​

rust
#[subscribe_message("kick")]
fn kick(&self, #[message_body("id", ParseIntPipe)] id: i64) -> HttpResult<()> {
    // ...
    Ok(())
}

#[message_body("id")] takes only data.id and applies the pipes that follow the name.

Guards before pipes ​

Guards run before pipes, so they see the unvalidated payload. If a guard needs a field, it must read it from ctx.switch_to_ws().get_data() and tolerate it being missing or having a different shape; Tertulia's RoomMemberGuard lets messages without room through so that the pipe is the one to report the error. See Guards.

Released under the MIT License.