Volga
Home
Agent Skill
API Docs
GitHub
  • English
  • Русский
Home
Agent Skill
API Docs
GitHub
  • English
  • Русский
  • Home
  • Getting Started

    • Quick Start
    • Handlers
    • Route Parameters
    • Query Parameters
    • Route Groups
  • Requests & Responses

    • Headers
    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Multipart Responses
    • Raw Request Body
    • Cookies
    • Validating Requests
  • Middleware & Infrastructure

    • Basic Middleware
    • Custom Middleware
    • Parameterized Middleware
    • Response Compression
    • Request Decompression
    • CORS (Cross-Origin Resource Sharing)
    • Static Files
    • Rate Limiting
    • Configuration Files
    • OpenAPI
  • Security & Access

    • Authentication and Authorization
    • OAuth 2.1 & OpenID Connect
    • OAuth 2.1 Client
    • Machine-to-Machine Grants
    • DPoP — Sender-Constrained Tokens
  • Reliability & Observability

    • Global Error Handling
    • Tracing & Logging
    • Request cancellation
    • Graceful Shutdown
  • Protocols & Realtime

    • HTTP Versions and Methods
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Advanced Patterns

    • Dependency Injection
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods
  • Agent Skill

WebSockets

Volga provides built-in support for WebSockets using a single, flexible API. This allows seamless connection handling at all levels, from establishing a connection to processing individual messages, with the ability to inject dependencies or access HTTP metadata.

Switching Between WebSockets and WebSocket-over-HTTP/2

If running under HTTP/2, Volga uses WebSocket-over-HTTP/2 by default and falls back to WebSockets when only an HTTP/1 connection is available. This behavior can be configured using feature flags.

WebSockets

[dependencies]
volga = { version = "...", features = ["ws"] }

WebSocket-over-HTTP/2

[dependencies]
volga = { version = "...", features = ["http2", "ws"] }

Simple Server

After updating Cargo.toml with the ws feature flag, you can implement a basic message handler using the map_msg() method. The following example responds with a formatted string containing the received message:

use volga::App;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Simple string message handler
    app.map_msg("/ws", |msg: String| async move {
        format!("Received: {}", msg)
    });
    
    app.run().await
}

A message handler with nothing to await can also return its reply directly, since v0.11.0:

app.map_msg("/ws", |msg: String| format!("echo: {msg}"));

The arguments after the message read the request that opened the connection, path parameters included — since 0.14.0, when map_ws and map_msg started handing their handler the parameters of its route:

app.map_msg("/ws/{room}", |msg: String, room: String| format!("[{room}] {msg}"));

JSON Messages

A message handler can take and return Json<T>. A Json<T> is received from a text or a binary frame, and is sent as a text frame — the reply of a map_msg handler as well as WebSocket::send and WsSink::send — so a browser client reads it from event.data as a string and passes it to JSON.parse:

use volga::{App, Json};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Request {
    text: String,
}

#[derive(Serialize)]
struct Reply {
    echo: String,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_msg("/ws", |msg: Json<Request>| {
        Json(Reply { echo: msg.text.clone() })
    });

    app.run().await
}

Message Types of Your Own

A message is anything implementing TryFrom<Message>, and a reply anything implementing TryInto<Message>. Since 0.12.0 their conversion error can be any type that converts into volga's Error — serde_json::Error, a (StatusCode, E) pair, a type of your own implementing IntoError — rather than Error itself. The same holds for WebSocket::on_msg, WebSocket::recv and send, WsStream::recv and WsSink::send.

use serde::Deserialize;
use volga::{App, ws::Message};

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum Command {
    Ping,
    Echo { text: String },
}

impl TryFrom<Message> for Command {
    type Error = serde_json::Error;

    fn try_from(msg: Message) -> Result<Self, Self::Error> {
        serde_json::from_slice(&msg.into_inner().into_data())
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // {"type":"ping"}                  -> pong
    // {"type":"echo","text":"hello"}   -> hello
    app.map_msg("/ws", |cmd: Command| match cmd {
        Command::Ping => "pong".to_string(),
        Command::Echo { text } => text,
    });

    // A `Message` is a reply too: this echo keeps text frames text and binary frames binary
    app.map_msg("/echo", |msg: Message| msg);

    app.run().await
}

Tips

A frame that does not convert into the handler's message type is skipped, and the connection stays open; with the tracing feature on, it is logged as an error. Where a client has to learn that its message was rejected, take a type that accepts any frame — Message, Bytes — and reply with the error yourself, or read the socket with recv(), which hands the conversion error to you.

These are simple examples; to get more control over a particular connection you may choose another method - map_ws().

use volga::{App, ws::WebSocket};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Simple WebSocket handler
    app.map_ws("/ws", |mut ws: WebSocket| async move {
        // Do something when a connection established

        ws.on_msg(|msg: String| async move {
            // Do something with a message

            format!("Received: {}", msg)
        }).await;
    });
    
    app.run().await
}

This example functions similarly to the first but offers greater control over the connection.

For advanced use cases, you can split the WebSocket into separate sender and receiver components using split():

use volga::{App, ws::{WebSocket, WsEvent}};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Simple WebSocket handler
    app.map_ws("/ws", |ws: WebSocket| async move {
        // Split socket into sender and receiver that can be used separately
        let (mut sender, mut receiver) = ws.split();

        tokio::spawn(async move {
            // `Message: TryFrom<&str>`, so the string goes in as it is
            let _ = sender.send("Hello from WebSockets server!").await;
        });

        tokio::spawn(async move {
            while let Some(Ok(msg)) = receiver.recv::<String>().await {
                match msg {
                    WsEvent::Data(msg) => println!("received: {msg}"),
                    WsEvent::Close(frame) => println!("close: {frame:?}"),
                    _ => (),
                }
            }
        });
    });
    
    app.run().await
}

This example sends a single message upon connection and logs incoming messages.

For full control, for instance, to configure a connection or specify some sub-protocols, there is another method - map_conn(), you may use it like this:

use volga::{App, ws::{WebSocketConnection, WebSocket}};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_conn("/ws", handle);

    app.run().await
}

async fn handle(conn: WebSocketConnection) -> HttpResult {
    // Here can be configured a connection and extract something from DI or HTTP metadata
    conn.with_protocols(["foo-ws"]).on(handle_socket)
}

async fn handle_socket(mut ws: WebSocket) {
    ws.on_msg(handle_message).await;
}

async fn handle_message(msg: String) -> String {
    format!("Received: {msg}")
}

Dependency Injection

You can inject any dependency from the DI container by using the Dc<T> at any layer in a regular way described here.

use volga::{App, ws::{WebSocketConnection, WebSocket}};
use std::sync::{Arc, RwLock};

type Counter = Arc<RwLock<i32>>;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.add_singleton(Counter::default());
    app.map_conn("/ws", handle);

    app.run().await
}

async fn handle(conn: WebSocketConnection, counter: Dc<Counter>) -> HttpResult {
    conn.with_protocols(["foo-ws"]).on(|ws| handle_socket(ws, counter))
}

async fn handle_socket(mut ws: WebSocket, counter: Dc<Counter>) {
    ws.on_msg(move |msg: String| handle_message(msg, counter.clone())).await;
}

async fn handle_message(msg: String, counter: Dc<Counter>) -> String {
    let mut value = counter.write().expect("Failed to lock counter");
    *value += 1;
    format!("Received: {msg}; Message #{value}")
}

You can find a complete example here.

Last Updated: 10/20/26, 3:26 PM
Prev
HTTPS
Next
Server-Sent Events (SSE)