Riverside
Elixir Library: Simple WebSocket Server Framework
Install / Use
/learn @lyokato/RiversideREADME
Riverside - Plain WebSocket Server Framework for Elixir
Installation
If available in Hex, the package can be installed
by adding riverside to your list of dependencies in mix.exs:
def deps do
[
{:riverside, "~> 2.2.1"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/riverside.
Version v2
This library had been updated only as a bugfix for a long time, but in the meantime, the version of Elixir, plug, cowboy, etc. had been upgraded, and the version of each library that riverside depends on had become outdated.
The version of each library that riverside depends on became outdated. riverside-v2 is the result of upgrading the dependent libraries and Elixir versions to more recent ones, and fixing the problems that occurred with riverside at that time. Although the version has been increased, there are no additional features.
The functionality is almost the same as v1, but the metrics-related features that existed in v1 have been removed in v2. This is because the libraries that were relied on for prometheus-related functions are now too old.
We decided to take this opportunity to focus on the minimum functionality in v2. If you need statistics, please provide them yourself.
Getting Started
Handler
At first, you need to prepare your own Handler module with use Riverside line.
in handle_message/3, process messages sent by client.
This doesn't depend on some protocol like Socket.io.
So do client-side, you don't need to prepared some libraries.
defmodule MySocketHandler do
# set 'otp_app' param like Ecto.Repo
use Riverside, otp_app: :my_app
@impl Riverside
def handle_message(msg, session, state) do
# `msg` is a 'TEXT' or 'BINARY' frame sent by client,
# process it as you like
deliver_me(msg)
{:ok, session, state}
end
end
Application child_spec
And in your Application module, set child spec for your supervisor.
defmodule MyApp do
use Application
def start(_type, _args) do
[
# ...
{Riverside, [handler: MySocketHandler]}
]
|> Supervisor.start_link([
strategy: :one_for_one,
name: MyApp.Supervisor
])
end
end
Configuration
config :my_app, MySocketHandler,
port: 3000,
path: "/my_ws",
max_connections: 10000, # don't accept connections if server already has this number of connections
max_connection_age: :infinity, # force to disconnect a connection if the duration passed. if :infinity is set, do nothing.
idle_timeout: 120_000, # disconnect if no event comes on a connection during this duration
reuse_port: false, # TCP SO_REUSEPORT flag
show_debug_logs: false,
transmission_limit: [
capacity: 50, # if 50 frames are sent on a connection
duration: 2000 # in 2 seconds, disconnect it.
],
cowboy_opts: [
#...
]
I’ll show you detailed description below. But you will know most of them when you see them.
Run
Launch your application, then the WebSocket service is provided with an endpoint like the following.
ws://localhost:3000/my_ws
And at the same time, we can also access to
http://localhost:3000/health
If you send a HTTP GET request to this URL, it returns response with status code 200, and text content "OK". This is just for health check.
This feature is defined in a Plug Router named Riverside.Router, and this is configured as default router param for child spec. So, you can defined your own Plug Router if you set as below.
In your Application module
defmodule MyApp do
use Application
def start(_type, _args) do
[
# ...
{Riverside, [
handler: MySocketHandler,
router: MyRouter, # Set your Plug Router here
]}
]
|> Supervisor.start_link([
strategy: :one_for_one,
name: MyApp.Spervisor
])
end
end
Handler's Callbacks
You can also define callback functions other than handle_message/3.
For instance, there are functions named init, terminate, and handle_info.
If you are accustomed to GenServer, you can easily imagine what they are,
though their interface is little bit different.
defmodule MySocketHandler do
use Riverside, otp_app: :my_app
@impl Riverside
def init(session, state) do
# initialization
{:ok, session, state}
end
@impl Riverside
def handle_message(msg, session, state) do
deliver_me(msg)
{:ok, session, state}
end
@impl Riverside
def handle_info(into, session, state) do
# handle message sent to this process
{:ok, session, state}
end
@impl Riverside
def terminate(reason, session, state) do
# cleanup
:ok
end
end
Authentication and Session
Here, I'll describe authenticate/1 callback function.
defmodule MySocketHandler do
use Riverside, otp_app: :my_app
@impl Riverside
def authenticate(req) do
{username, password} = req.basic
case MyAuthenticator.authenticate(username, password) do
{:ok, user_id} ->
state = %{}
{:ok, user_id, state}
{:error, :invalid_password} ->
error = auth_error_with_code(401)
{:error, error}
end
end
@impl Riverside
def init(session, state) do
{:ok, session, state}
end
@impl Riverside
def handle_message(msg, session, state) do
deliver_me(msg)
{:ok, session, state}
end
@impl Riverside
def handle_info(into, session, state) do
{:ok, session, state}
end
@impl Riverside
def terminate(reason, session, state) do
:ok
end
end
The argument of authenticate/1 is a struct of Riverside.AuthRequest.t.
And it has Map members
- queries: Map includes HTTP request's query params
- headers: Map includes HTTP headers
# When client access with a URL such like ws://localhost:3000/my_ws?token=FOOBAR,
# And you want to authenticate the `token` parameter ("FOOBAR", this time)
@impl Riverside
def authenticate(req) do
# You can pick the parameter like as below
token = req.queries["token"]
# ...
end
# Or else you want to authenticate with `Authorization` HTTP header.
@impl Riverside
def authenticate(req) do
# You can pick the header value like as below
auth_header = req.headers["authorization"]
# ...
end
The fact is that, you don't need to parse Authorization header by yourself, if you want to do Basic or Bearer authentication.
# Pick up `username` and `password` from `Basic` Authorization header.
# If it doesn't exist, `username` and `password` become empty strings.
@impl Riverside
def authenticate(req) do
{username, password} = req.basic
# ...
end
# Pick up token value from `Bearer` Authorization header
# If it doesn't exist, `token` become empty string.
@impl Riverside
def authenticate(req) do
token = req.bearer_token
# ...
end
Authentication failure
If authentication failure, you need to return {:error, Riverside.AuthError.t}.
You can build Riverside.AuthError struct with auth_error_with_code/1.
Pass proper HTTP status code.
@impl Riverside
def authenticate(req) do
token = req.bearer_token
case MyAuth.authenticate(token) do
{:error, :invalid_token} ->
error = auth_error_with_code(401)
{:error, error}
# _ -> ...
end
end
You can use put_auth_error_header/2 to put response header
error = auth_erro_with_code(400)
|> puth_auth_error_header("WWW-Authenticate", "Basic realm=\"example.org\"")
And two more shortcuts, put_auth_error_basic_header and put_auth_error_bearer_header.
error = auth_erro_with_code(401)
|> puth_auth_error_basic_header("example.org")
# This puts `WWW-Authenticate: Basic realm="example.org"`
error = auth_erro_with_code(401)
|> puth_auth_error_bearer_header("example.org")
# This puts `WWW-Authenticate: Bearer realm="example.org"`
error = auth_erro_with_code(400)
|> puth_auth_error_bearer_header("example.org", "invalid_token")
# This puts `WWW-Authenticate: Bearer realm="example.org", error="invalid_token"`
Successful authentication
@impl Riverside
def authenticate(req) do
token = req.bearer_token
case MyAuth.authenticate(token) do
{:ok, user_id} ->
session_id = create_random_string()
state = %{}
{:ok, user_id, session_id, state}
# _ -> ...
end
end
If authentication results in success, return {:ok, user_id, session_id, state}.
You can put any data into state, same as you do in init in GenServer.
session_id should be random string. You also can return {:ok, user_id, state}, and
Then session_id will be generated automatically.
And init/3 will be called after successful auth response.
session
Now I can describe about the session parameter included for each callback functions.
This is a Riverside.Session.t struct, and it includes some parameters like user_id and session_id.
When you omit to define authenticate/1, both user_id and session_id will be set random value.
@impl Riverside
def handle_message(msg, session, state) do
# session.user_id
# session.session_id
end
Message and Delivery
Message Format
If a client sends a simple TEXT frame with JSON format like the following
{
"to": 1111,
"body": "Hello"
}
You can handle this JSON message as a Map.
@impl Riverside
def handle_message(incoming_message, session, state) do
dest_user_id = incoming_message["to"]
body = incoming_message["body"]
outgoing_message = %{
"from" => "#{session.user_id}",
"body" => body,
}
deliver_user(dest_user_id, out
Related Skills
node-connect
344.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
96.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
