Wire formats

Copy Markdown

ChannelClient ships a pluggable format layer: a single ChannelClient.Format behaviour owns both the structure of frames and their encoding. The default is Phoenix Channels JSON, but ETF (Erlang terms) works out of the box, and MessagePack or anything else is a few lines away.

Built-in formats

FormatOptionNotes
JSON v2format: :json (default)JSON arrays; protocol "2.0.0"
JSON v1vsn: "1.0.0" or format optsJSON objects; legacy servers
ETFformat: :etfErlang External Term Format, v2 array shape
TOONformat: :toonToken-Oriented Object Notation (text)
BTOONformat: :btoonBinary variant of TOON

ETF is a zero-dependency binary format — ideal for Elixir/Erlang service meshes. Decoding runs in binary_to_term/2 safe mode, so hostile peers cannot create atoms on your client.

# Default JSON
{:ok, socket} = ChannelClient.Socket.start_link(url: "ws://...")

# ETF
{:ok, socket} =
  ChannelClient.Socket.start_link(url: "ws://...", format: :etf)

# JSON v1 with an alternate library
{:ok, socket} =
  ChannelClient.Socket.start_link(
    url: "ws://...",
    format: {ChannelClient.Formats.JSON,
             version: "1.0.0", json_library: MyApp.JsonLib}
  )

TOON & BTOON

TOON (Token-Oriented Object Notation) is a compact text encoding of JSON-shaped data; BTOON is its binary counterpart. Both are built in, and they delegate the actual codec to the toon_ex package — an optional dependency of channel_client:

{:toon_ex, "~> 1.5"}
# Uses ToonEx / ToonEx.Btoon from toon_ex by default:
ChannelClient.Socket.start_link(url: "ws://...", format: :toon)
ChannelClient.Socket.start_link(url: "ws://...", format: :btoon)

# Or point at any equivalent codec explicitly:
ChannelClient.Socket.start_link(
  url: "ws://...",
  format: {ChannelClient.Formats.TOON, library: MyApp.MyToonCodec}
)

The codec contract is minimal — encode!/1 and decode!/1. Frames are maps with :join_ref, :ref, :topic, :event and :payload keys; codecs may return atom- or string-keyed maps on decode. Payloads must be JSON-compatible data (that is what TOON represents). If no usable codec is loaded at startup you get an ArgumentError naming the missing library — fail fast instead of failing mid-connection.

The legacy options keep working exactly as before: :vsn selects the JSON protocol version and :json_library (alias :serializer) swaps the JSON codec. An explicit :format takes precedence.

Server-side note

Your Phoenix endpoint decides which formats it accepts. JSON v2 is the default on both sides. For ETF you must register an ETF serializer on the server socket before clients can connect with format: :etf.

Writing a custom format

Implement ChannelClient.Format:

defmodule MyApp.MsgPack do
  @behaviour ChannelClient.Format

  @impl true
  def init(opts), do: opts

  @impl true
  def encode!(%ChannelClient.Message{} = msg, _opts) do
    Msgpax.pack!([msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload])
  end

  @impl true
  def decode!(payload, _opts) when is_binary(payload) do
    [join_ref, ref, topic, event, payload | _] = Msgpax.unpack!(payload)
    %ChannelClient.Message{
      join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload
    }
  end
end
ChannelClient.Socket.start_link(
  url: "ws://localhost:4000/socket/websocket",
  format: {MyApp.MsgPack, compress: true}
)

Contract details:

  • Return the encoded frame as iodata/binary from encode!/2.
  • Raise in encode!/2 for payloads you cannot serialize — synchronous pushes surface this as {:error, reason}, async pushes log and drop.
  • Raise in decode!/2 for malformed input — those frames are logged and dropped instead of crashing the socket.
  • init/1 validates options once at socket startup; prefer failing there.
  • The socket stamps ref and join_ref after outbound plugs run and before encoding, so protocol framing stays correct regardless of format.

Format × plugs × telemetry

Formats compose with everything else in the pipeline: plugs operate on decoded %ChannelClient.Message{} structs (before encoding / after decoding), and telemetry measurements like payload_bytes reflect whatever bytes your format produces.