howdy/websocket
WebSockets as ordinary routes.
A WebSocket starts life as a GET request, so it is added to a
controller like any other route and goes through the same middleware,
guard and versioning. The handler builds a socket description and
finishes with upgrade.
import howdy/websocket
controller.new("echo")
|> controller.get("/", fn(ctx) {
websocket.new(fn(_socket) { 0 })
|> websocket.on_text(fn(socket, count, text) {
let _ = websocket.send_text(socket, text)
websocket.continue(count + 1)
})
|> websocket.upgrade(ctx)
})
Every callback runs in the connection’s own process and receives the
Socket to send frames on, the current state, and returns what to do
next: continue with new state, or close.
Other processes can reach a socket through subject, and groups of
sockets through howdy/websocket/channel.
Browsers cannot set headers on a WebSocket handshake, so guards for socket routes usually read a cookie or a query parameter instead.
Types
A socket description under construction. state is what the callbacks
carry between messages; msg is what subject accepts.
pub opaque type Builder(state, msg)
The status code a close frame carries. NormalClosure is the usual
choice.
pub type CloseCode {
NormalClosure
GoingAway
ProtocolError
UnsupportedData
InvalidPayloadData
PolicyViolation
MessageTooBig
InternalError
ServiceRestart
TryAgainLater
BadGateway
ApplicationCode(code: Int)
}
Constructors
-
NormalClosureThe connection did what it was for and is closing normally (1000).
-
GoingAwayThe server is going away, such as shutting down (1001).
-
ProtocolErrorThe client broke the protocol (1002).
-
UnsupportedDataData arrived that the server cannot accept (1003).
-
InvalidPayloadDataA message did not match the type it declared, such as a text frame that is not UTF-8 (1007).
-
PolicyViolationThe client broke your rules when no more specific code applies (1008).
-
MessageTooBigA message was larger than the server will handle (1009).
-
InternalErrorSomething went wrong on the server (1011).
-
ServiceRestartThe server is restarting and clients may reconnect shortly (1012).
-
TryAgainLaterThe server is overloaded and the client should retry later (1013).
-
BadGatewayAn upstream server answered badly (1014).
-
ApplicationCode(code: Int)An application specific code, between 3000 and 4999.
A frame to send to a client, as used by howdy/websocket/channel.
pub type Frame {
Text(String)
Binary(BitArray)
}
Constructors
-
Text(String) -
Binary(BitArray)
Values
pub fn allow_origins(
builder: Builder(state, msg),
origins: List(String),
) -> Builder(state, msg)
Replace the default same-origin policy with an exact HTTP(S) allowlist,
for pages served from somewhere else, such as a separate frontend. A
proxy that ends TLS needs none: the default follows it (see upgrade).
Invalid entries panic as configuration errors.
pub fn close(
socket: Socket(msg),
code: CloseCode,
reason: String,
) -> Next(state)
Send a close frame with the given code and reason, then end the
connection. on_close still runs. Nothing can be sent afterwards.
pub fn continue(state: state) -> Next(state)
Keep the connection open and handle the next message with state.
pub fn new(
on_open: fn(Socket(msg)) -> state,
) -> Builder(state, msg)
Describe a socket. on_open runs once the handshake is done and returns
the starting state. Frames and messages are ignored until a callback for
them is added.
pub fn on_binary(
builder: Builder(state, msg),
handler: fn(Socket(msg), state, BitArray) -> Next(state),
) -> Builder(state, msg)
Handle binary frames from the client.
pub fn on_close(
builder: Builder(state, msg),
handler: fn(Socket(msg), state) -> Nil,
) -> Builder(state, msg)
Run once when the connection ends, however that happens. Nothing can be sent to the client from here.
pub fn on_json(
builder: Builder(state, msg),
decoder: decode.Decoder(a),
handler: fn(Socket(msg), state, a) -> Next(state),
) -> Builder(state, msg)
Handle text frames by decoding them as JSON. Frames that fail to decode
go to on_invalid, which by default ignores them. Replaces any on_text
handler.
pub fn on_json_or(
builder: Builder(state, msg),
decoder: decode.Decoder(a),
handler: fn(Socket(msg), state, a) -> Next(state),
on_invalid: fn(Socket(msg), state, json.DecodeError) -> Next(
state,
),
) -> Builder(state, msg)
Like on_json, but with a handler for frames that fail to decode. A
common choice is to close with InvalidPayloadData:
|> websocket.on_json_or(decoder, handle, fn(socket, _state, _error) {
websocket.close(socket, websocket.InvalidPayloadData, "expected json")
})
pub fn on_message(
builder: Builder(state, msg),
handler: fn(Socket(msg), state, msg) -> Next(state),
) -> Builder(state, msg)
Handle messages sent to the socket’s subject by other processes.
pub fn on_text(
builder: Builder(state, msg),
handler: fn(Socket(msg), state, String) -> Next(state),
) -> Builder(state, msg)
Handle text frames from the client.
pub fn require_origin(
builder: Builder(state, msg),
) -> Builder(state, msg)
Require Origin even for non-browser clients. By default, absent Origin is accepted for CLI clients; supplied, malformed or duplicate origins are always checked. Origin policy is not a substitute for authentication.
pub fn send_binary(
socket: Socket(msg),
data: BitArray,
) -> Result(Nil, content.SendError)
Send a binary frame.
pub fn send_frame(
socket: Socket(msg),
frame: Frame,
) -> Result(Nil, content.SendError)
Send a Frame.
pub fn send_json(
socket: Socket(msg),
body: json.Json,
) -> Result(Nil, content.SendError)
Send JSON as a text frame.
pub fn send_text(
socket: Socket(msg),
text: String,
) -> Result(Nil, content.SendError)
Send a text frame.
pub fn subject(socket: Socket(msg)) -> process.Subject(msg)
A subject other processes can send to. Each message arrives at the
on_message callback. Hand it to whatever needs to push to this client.
pub fn upgrade(
builder: Builder(state, msg),
ctx: context.Context(guarded),
) -> response.Response(content.Content)
Upgrade the request. The connection then runs until a callback returns
close or stop, or the client goes away.
A request that is not a valid handshake is answered with 400. In tests
there is no connection to upgrade, so the answer is 426 Upgrade Required.
Disallowed origins receive 403 before the handshake or on_open. By
default the browser’s origin must be the one it reached the app at:
the request’s scheme, host and port, or, behind a proxy, the scheme in
x-forwarded-proto and the host in x-forwarded-host. A hostile page
cannot set those on a browser’s handshake, so trusting them for this
check gives nothing away.