roadrunner_ws_handler behaviour (roadrunner v0.1.0)

View Source

Behaviour for WebSocket handlers.

After a successful upgrade — signaled by a regular roadrunner_handler returning {websocket, Module, State} — the connection process drives a frame loop that calls handle_frame/2 for each non-control frame received from the client.

Control frames (ping/close) are auto-handled by the connection layer and never reach the user callback. The handler may reply with zero or more frames (each {Opcode, Payload} — always Fin = true) or signal close to terminate the session gracefully:

  • {close, State} sends an empty close frame (RFC 6455 §5.5.1 permits this).
  • {close, Code, Reason, State} sends a close frame carrying the status code and reason text. Code must be a code the spec permits the server to send (1000-1003, 1007-1011, 1014, or 3000-4999 per RFC 6455 §7.4). Reason is iodata that must be valid UTF-8. The framework crashes the session on invalid Code / Reason rather than emit a malformed close frame.

State is passed through unchanged by the framework.

The 4-tuple {reply, Frames, NewState, Opts} and 3-tuple {ok, NewState, Opts} variants accept an opt list. Currently recognized opt:

  • hibernate — after the framework finishes processing this event (and any other queued events), the session process hibernates until the next inbound frame. Useful for mostly-idle WebSocket endpoints (chat, notifications, LiveView channels) — drops the per-process heap to ~1KB and saves significant memory at scale. Hibernation has a per-wake CPU cost (~tens of microseconds for the GC), so don't enable it for high-frequency frame patterns.

The optional init/1 callback runs once in the session process after the 101 has been written to the wire and before the first frame is read. It receives the state passed via the upgrade tuple and may emit zero or more frames or close immediately. Use it to register pubsub subscriptions, start linked workers, or push priming frames at connect-time (e.g. a snapshot the client expects before sending its first frame).

The optional handle_info/2 callback receives any Erlang message delivered to the session process that is not a transport active-mode tuple (data/closed/error). Use it for pubsub / asynchronous push patterns where the handler subscribes to topics in init/1 or handle_frame/2 and forwards inbound messages to the WebSocket peer. Handlers that don't export this callback have unknown messages dropped silently.

A terminate callback is intentionally NOT provided — most handlers don't need bespoke teardown, and the conn process's drain plus listener slot reconciliation cover the lifecycle bookkeeping.

All callbacks share the same return shape — {reply, Frames, NewState}, {ok, NewState}, {close, NewState}, or {close, Code, Reason, NewState}, optionally with a 4-tuple Opts list for hibernate on the reply / ok shapes.

Summary

Types

Per-event option flags. Currently only hibernate is recognized; when included in a 4-tuple callback return, the framework hibernates the session process after the event finishes processing.

Unified return shape for all roadrunner_ws_handler callbacks.

Callbacks

Invoked for each non-control frame received from the client, after the framework has reassembled fragmented messages (so the handler always sees a complete text or binary payload). Returns one of the result/0 shapes: reply with frames, stay open, close gracefully, or close with a code + reason. Control frames (ping/pong/close) are auto-handled by the framework and never reach this callback.

Optional. Receives any Erlang message delivered to the session process that isn't a transport active-mode event. Use for pubsub / asynchronous push patterns. Returns the same shape as handle_frame/2. Handlers that don't export this callback have unknown messages dropped silently.

Optional. Runs once in the session process after the 101 has been written to the wire and before the first frame is read, with the state from the upgrade tuple {websocket, Module, State}. Use it to register pubsub subscriptions, start linked workers, or push priming frames at connect-time. Returns the same shape as handle_frame/2. Handlers that don't export this callback skip the init step.

Types

opt()

-type opt() :: hibernate.

Per-event option flags. Currently only hibernate is recognized; when included in a 4-tuple callback return, the framework hibernates the session process after the event finishes processing.

result()

-type result() ::
          {reply, Frames :: [{roadrunner_ws:opcode(), iodata()}], NewState :: term()} |
          {reply, Frames :: [{roadrunner_ws:opcode(), iodata()}], NewState :: term(), [opt()]} |
          {ok, NewState :: term()} |
          {ok, NewState :: term(), [opt()]} |
          {close, NewState :: term()} |
          {close, Code :: roadrunner_ws:close_code(), Reason :: iodata(), NewState :: term()}.

Unified return shape for all roadrunner_ws_handler callbacks.

  • {reply, Frames, NewState} — emit each {Opcode, Payload} frame (always with FIN=true) and keep the session open.
  • {reply, Frames, NewState, Opts} — same, with an opt list (e.g. [hibernate]).
  • {ok, NewState} — no outbound frames, keep the session open.
  • {ok, NewState, Opts} — same, with an opt list.
  • {close, NewState} — send an empty close frame and terminate.
  • {close, Code, Reason, NewState} — send a close frame carrying Code (RFC 6455 §7.4 server-permitted code) and UTF-8 Reason. The framework crashes the session if Code / Reason is invalid rather than emit a malformed close.

Callbacks

handle_frame(Frame, State)

-callback handle_frame(Frame :: roadrunner_ws:frame(), State :: term()) -> result().

Invoked for each non-control frame received from the client, after the framework has reassembled fragmented messages (so the handler always sees a complete text or binary payload). Returns one of the result/0 shapes: reply with frames, stay open, close gracefully, or close with a code + reason. Control frames (ping/pong/close) are auto-handled by the framework and never reach this callback.

handle_info(Info, State)

(optional)
-callback handle_info(Info :: term(), State :: term()) -> result().

Optional. Receives any Erlang message delivered to the session process that isn't a transport active-mode event. Use for pubsub / asynchronous push patterns. Returns the same shape as handle_frame/2. Handlers that don't export this callback have unknown messages dropped silently.

init(State)

(optional)
-callback init(State :: term()) -> result().

Optional. Runs once in the session process after the 101 has been written to the wire and before the first frame is read, with the state from the upgrade tuple {websocket, Module, State}. Use it to register pubsub subscriptions, start linked workers, or push priming frames at connect-time. Returns the same shape as handle_frame/2. Handlers that don't export this callback skip the init step.