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

  • NormalClosure

    The connection did what it was for and is closing normally (1000).

  • GoingAway

    The server is going away, such as shutting down (1001).

  • ProtocolError

    The client broke the protocol (1002).

  • UnsupportedData

    Data arrived that the server cannot accept (1003).

  • InvalidPayloadData

    A message did not match the type it declared, such as a text frame that is not UTF-8 (1007).

  • PolicyViolation

    The client broke your rules when no more specific code applies (1008).

  • MessageTooBig

    A message was larger than the server will handle (1009).

  • InternalError

    Something went wrong on the server (1011).

  • ServiceRestart

    The server is restarting and clients may reconnect shortly (1012).

  • TryAgainLater

    The server is overloaded and the client should retry later (1013).

  • BadGateway

    An 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)

What a callback asks the socket to do once it is done.

pub opaque type Next(state)

A handle for one open connection. msg is the type of message other processes may send it; see subject.

pub opaque type Socket(msg)

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 stop() -> Next(state)

End the connection without telling the client why.

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.

✨ Search Document