How to Implement WebSockets in Topcoat: A Complete Guide with Examples

Topcoat provides first-class WebSocket support through the topcoat-router crate, gated by the websocket feature.

To implement WebSockets in Topcoat, you use the WebSocketUpgrade extractor to handle the handshake and spawn async tasks for message exchange. The tokio-rs/topcoat repository implements this through the topcoat-router crate, where WebSocket endpoints behave like standard HTTP routes but receive a WebSocketUpgrade that transitions the connection after protocol negotiation. Once upgraded, the WebSocket type implements both Stream and Sink traits, allowing you to recv().await incoming frames and send(msg).await responses using the Message enum.

Enabling WebSocket Support

WebSocket functionality lives in the topcoat-router crate and must be enabled via the websocket feature flag in your Cargo.toml:

[dependencies]
topcoat-router = { version = "*", features = ["websocket"] }

This exposes the WebSocketUpgrade extractor and WebSocket connection type under topcoat::router::content::websocket.

The WebSocket Handshake Flow

Topcoat handles the WebSocket handshake through three distinct phases managed by the WebSocketUpgrade extractor in src/content/websocket/upgrade.rs.

Request Validation

When a client sends a GET request with Upgrade: websocket headers, the WebSocketUpgrade extractor validates the handshake automatically. It rejects non-GET methods with a 405 Method Not Allowed status and malformed handshakes with 400 Bad Request, ensuring only valid WebSocket connections proceed.

Protocol Upgrade

Inside your route handler, calling WebSocketUpgrade::on_upgrade initiates the handshake response while spawning an independent async task. The closure passed to on_upgrade receives a fully established WebSocket instance once the client switches protocols, allowing the route to return the HTTP response immediately without blocking.

use topcoat::{
    Result,
    router::{
        content::websocket::{Message, WebSocketUpgrade},
        response::Response,
        route,
    },
};

#[route(GET "/ws/echo")]
async fn echo(upgrade: WebSocketUpgrade) -> Result<Response> {
    upgrade.on_upgrade(|mut socket| async move {
        while let Some(Ok(msg)) = socket.recv().await {
            if matches!(msg, Message::Text(_) | Message::Binary(_)) {
                if socket.send(msg).await.is_err() {
                    break;
                }
            }
        }
    })
}

Handling Messages with the WebSocket Type

The WebSocket type in src/content/websocket/socket.rs implements Stream<Item = Result<Message>> and Sink<Message>, providing idiomatic async Rust interfaces for bidirectional communication.

Message Types

The Message enum defined in src/content/websocket/message.rs covers all WebSocket frame types:

  • Text(String) – UTF-8 encoded application data
  • Binary(Vec<u8>) – Raw binary application data
  • Ping – Automatic heartbeat requests (automatically answered by the server, but still emitted for monitoring)
  • Pong – Heartbeat responses
  • Close – Connection termination, optionally carrying a CloseFrame with status code and reason

Reading and Writing Messages

Use recv().await to read incoming frames and send(msg).await to transmit data. The connection remains open until you call socket.close().await or the client sends a close frame.

use futures_util::{StreamExt, SinkExt};
use topcoat::router::content::websocket::WebSocketUpgrade;

#[route(GET "/ws/chat")]
async fn chat(upgrade: WebSocketUpgrade) -> Result<Response> {
    upgrade.on_upgrade(|socket| async move {
        let (mut sender, mut receiver) = socket.split();
        
        while let Some(Ok(msg)) = receiver.next().await {
            let _ = sender.send(msg).await;
        }
    })
}

Configuring Sub-Protocols and Limits

The WebSocketUpgrade builder pattern allows fine-grained control over the handshake via methods defined in src/content/websocket/upgrade.rs.

Sub-Protocol Negotiation

Declare supported sub-protocols using protocols(). Topcoat selects the first match from the client's request and echoes it back, accessible via socket.protocol():

#[route(GET "/ws/protocol")]
async fn protocol(upgrade: WebSocketUpgrade) -> Result<Response> {
    upgrade
        .protocols(&["json", "chat"])
        .on_upgrade(|socket| async move {
            if let Some(proto) = socket.protocol() {
                println!("Negotiated: {}", proto.to_str().unwrap());
            }
        })
}

Security Limits

Protect against resource exhaustion using size guards:

  • max_message_size(256 * 1024) – Rejects payloads exceeding 256 KiB
  • max_frame_size – Limits individual frame sizes
  • max_write_buffer_size – Prevents unbounded memory growth when clients stop reading

Key Source Files in tokio-rs/topcoat

Understanding the implementation details helps when debugging or extending functionality:

Summary

  • Enable WebSocket support via the websocket feature in topcoat-router
  • Extract the handshake using WebSocketUpgrade in route handlers
  • Upgrade connections with on_upgrade() to spawn async message handling tasks
  • Exchange data using recv() and send() on the WebSocket type, which implements Stream/Sink
  • Configure sub-protocols and size limits using the builder pattern on WebSocketUpgrade
  • Reference the source in src/content/websocket/ for implementation details of the socket and message types

Frequently Asked Questions

How do I enable WebSocket support in Topcoat?

Add the websocket feature to your topcoat-router dependency in Cargo.toml. This gates the WebSocketUpgrade extractor and related types to keep compile times minimal when WebSocket functionality is not needed.

What is the difference between WebSocketUpgrade and WebSocket?

WebSocketUpgrade is the extractor that handles the initial HTTP handshake and validation. It exists only during the request phase. WebSocket represents the established bidirectional stream after the handshake completes, providing the recv() and send() methods for message exchange in the spawned async task.

How does Topcoat handle incoming Ping frames?

According to the implementation in src/content/websocket/socket.rs, incoming Ping frames are automatically answered with Pong frames at the protocol level. However, they are still emitted as Message::Ping items through the Stream implementation, allowing your application to monitor or log heartbeat activity if desired.

Can I use middleware with WebSocket routes in Topcoat?

Yes. Because WebSocketUpgrade functions as a standard extractor, you can compose it with session extractors, cookie parsers, or authentication middleware before calling on_upgrade(). The middleware runs during the HTTP phase, allowing you to reject unauthorized requests before the WebSocket handshake occurs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →