Skip to main content

kemal-websocket

Implementing real-time bi-directional communication with WebSockets in Kemal, origin security, and lifecycle management.

Source facts

Repository
kemalcr/kemal
Last source activity
August 24, 2026 at 11:28
Detected SKILL.md language
English
Stars
3,923
Forks
206

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
kemal-websocket
description
Implementing real-time bi-directional communication with WebSockets in Kemal, origin security, and lifecycle management.
license
MIT
# Kemal WebSocket Integration This skill provides expert guidance on using WebSockets for real-time features in Kemal, with version-aware origin security guidance, strictly following patterns from [`kemal-by-example/real-time-dashboard`](https://github.com/sdogruyol/kemal-by-example/tree/master/real-time-dashboard) and [`kemal-by-example/twitter-clone`](https://github.com/sdogruyol/kemal-by-example/tree/master/twitter-clone). ## Version Notes - **Default Origin policy changed in 1.13.0:** with an empty `websocket_allowed_origins` (the default), Kemal 1.13.0+ enforces same-origin and rejects missing or mismatched `Origin` headers with `403 Forbidden`; releases up to 1.12.0 allow **all** origins (CSWSH risk — configure an allowlist explicitly, see below). - `websocket_allowed_origins = ["*"]` opt-in allow-all: since Kemal 1.13.0. - Non-GET upgrade rejection (RFC 6455 §4.1, `405 Method Not Allowed` + `Allow: GET`) and explicit `Connection: close` after a rejected handshake: since Kemal 1.13.0. ## Core Mandates - **WebSocket Route Definition:** Use the `ws` helper to define WebSocket endpoints: ```crystal ws "/socket" do |socket, env| # Socket lifecycle callbacks end ``` - **Origin Validation & CSWSH Security:** - **CRITICAL ON STABLE RELEASES (1.12.0 and earlier):** An empty `Kemal.config.websocket_allowed_origins = [] of String` (default) **permits all origins** (`return true if allowed.empty?`). To protect against Cross-Site WebSocket Hijacking (CSWSH) in production on these releases, you **MUST explicitly configure allowed origins**: ```crystal # MANDATORY on 1.12.0 and earlier to protect against CSWSH: Kemal.config.websocket_allowed_origins = %w[https://myapp.com http://localhost:3000] ``` - **ON KEMAL 1.13.0+:** An empty `websocket_allowed_origins` defaults to strict **same-origin** enforcement (matching request `Host`). Missing or mismatched `Origin` headers are rejected with `403 Forbidden`. To explicitly allow all origins: ```crystal # Kemal 1.13.0+ — opt-in allow-all: Kemal.config.websocket_allowed_origins = ["*"] ``` - **Protocol Compliance (RFC 6455 §4.1) — *[Kemal 1.13.0+]*:** - On Kemal 1.13.0+, WebSocket upgrade handshakes require the `GET` method. Non-GET upgrade requests (`POST`, `QUERY`, etc.) are automatically rejected with `405 Method Not Allowed`, `Allow: GET`, and `Connection: close`. - Failed handshakes close the connection immediately to prevent WebSocket connection smuggling. - **Socket Lifecycle Callbacks:** - `socket.on_message`: Handle incoming text messages from client - `socket.on_close`: Handle socket disconnection and perform cleanup - `socket.on_ping` / `socket.on_pong`: Handle heartbeat signals - **Concurrency Safety:** Always protect shared socket collections with a `Mutex`: ```crystal class Hub @sockets = Array(HTTP::WebSocket).new @mutex = Mutex.new def register(socket) @mutex.synchronize { @sockets << socket } end def unregister(socket) @mutex.synchronize { @sockets.delete(socket) } end end ``` - **Error Handling:** Rescue `IO::Error | Socket::Error` when sending to sockets and unregister closed sockets. ## Patterns from Source Code ### Centralized WebSocket Hub (real-time-dashboard/src/services/dashboard_hub.cr) ```crystal require "kemal" module RealTimeDashboard class DashboardHub # NOTE: Do not `include JSON::Serializable` here — it removes the default # constructor, and sockets/mutexes are not serializable anyway. Serialize a # plain stats payload (e.g. `{"total_requests" => total_requests}.to_json`) # when broadcasting instead. getter total_requests : Int64 = 0_i64 getter active_users : Int32 = 0 @sockets = Array(HTTP::WebSocket).new @mutex = Mutex.new def register(socket : HTTP::WebSocket) @mutex.synchronize { @sockets << socket } end def unregister(socket : HTTP::WebSocket) @mutex.synchronize { @sockets.delete(socket) } end def broadcast(message : String) @mutex.synchronize do @sockets.reject! do |socket| begin socket.send(message) false rescue IO::Error | Socket::Error true # Remove failed socket end end end end end end ``` ### WebSocket Route Setup with Origin Security ```crystal require "kemal" # Configure allowed origins explicitly (mandatory on 1.12.0 and earlier for security, recommended everywhere) Kemal.config.websocket_allowed_origins = %w[http://127.0.0.1:3000 http://localhost:3000] HUB = RealTimeDashboard::DashboardHub.new ws "/dashboard/socket" do |socket, env| HUB.register(socket) socket.on_message do |msg| # Handle incoming client messages (spawn long-running work to avoid blocking fiber) spawn do # process msg end end socket.on_close do HUB.unregister(socket) end end ``` ## Best Practices - **Security First:** Always configure `Kemal.config.websocket_allowed_origins` explicitly in production. Origin validation is a browser boundary, not authentication/authorization. - **Mutex Synchronization:** Wrap all modifications and iterations over socket lists in `@mutex.synchronize`. - **Selective Rejection:** Use `@sockets.reject!` inside broadcast loops to cleanly strip dead sockets without deadlocks. - **Graceful Teardown:** Unregister sockets in `on_close` callbacks. ## When to Use - When building real-time bi-directional chat applications, collaborative tools, or active telemetry dashboards. - When client and server communicate via continuous socket connections.
View on GitHub