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 and kemal-by-example/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 mismatchedOriginheaders with403 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 explicitConnection: closeafter a rejected handshake: since Kemal 1.13.0.
Core Mandates
WebSocket Route Definition: Use the
wshelper to define WebSocket endpoints:ws "/socket" do |socket, env| # Socket lifecycle callbacks endOrigin 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:# 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_originsdefaults to strict same-origin enforcement (matching requestHost). Missing or mismatchedOriginheaders are rejected with403 Forbidden. To explicitly allow all origins:# 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
GETmethod. Non-GET upgrade requests (POST,QUERY, etc.) are automatically rejected with405 Method Not Allowed,Allow: GET, andConnection: close. - Failed handshakes close the connection immediately to prevent WebSocket connection smuggling.
- On Kemal 1.13.0+, WebSocket upgrade handshakes require the
Socket Lifecycle Callbacks:
socket.on_message: Handle incoming text messages from clientsocket.on_close: Handle socket disconnection and perform cleanupsocket.on_ping/socket.on_pong: Handle heartbeat signals
Concurrency Safety: Always protect shared socket collections with a
Mutex: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 endError Handling: Rescue
IO::Error | Socket::Errorwhen sending to sockets and unregister closed sockets.
Patterns from Source Code
Centralized WebSocket Hub (real-time-dashboard/src/services/dashboard_hub.cr)
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
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_originsexplicitly 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_closecallbacks.
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.