Action Cable Protocol

Action Cable is a framework that allows you to integrate WebSockets with the rest of your Rails application easily.

It uses a simple JSON-based protocol for client-server communication.

AnyCable also implements an extended version of the protocol to provide better consistency guarantees.

Messages

Communication is based on messages. Every message is an object.

Protocol-related messages from server to client MUST have type field (string).

Possible types:

  • [welcome]
  • [disconnect]
  • [ping]
  • [confirm_subscription]
  • [reject_subscription]

There are also data messages–broadcasts and transmissions–they MUST have message field.

Protocol-related messages from client to server MUST have command field (string).

Possible commands:

  • [subscribe]
  • [unsubscribe]
  • [message]

Handshake

When client connects to server one of the following two could happen:

  • server accepts the connection and responds with welcome message ({"type":"welcome"})
  • server rejects the connection and responds with a disconnect message, which may include fields reason and reconnect ({"type":"disconnect", "reason":"unauthorized", "reconnect":false})*

Server MUST respond with either a welcome message or a disconnect message.

* disconnect message only exists in Rails 6.0 and later. Prior to 6.0, server would drop the connection without sending anything.

Subscriptions & identifiers

Data messages, client-to-server messages and some server-to-client messages (confirm_subscription, reject_subscription) MUST contain identifier field (string) which is used to route data to the specified channel.

It's up to server and client how to generate and resolve identifiers.

Rails identifiers schema is the following: { channel: "MyChannelClass", **params }.to_json.

For example, to subscribe to ChatChannel with id: 42 client should send the following message:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "subscribe"
}

The response from server MUST contain the same identifier, e.g.:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "type": "confirm_subscription"
}

To unsubscribe from the channel client should send the following message:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "unsubscribe"
}

There is no unsubscription confirmation sent (see PR#24900).

Receive messages

Data message from server to client MUST contain identifier field and message field with the data itself.

Perform actions

Action message from client to server MUST contain command ("message"), identifier fields, and data field containing a JSON-encoded value.

The data field MAY contain action field.

For example, in Rails to invoke a method on a channel class, you should send:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "message",
  "data": "{\"action\":\"speak\",\"text\":\"hello!\"}"
}

Ping

Although WebSocket protocol describes low-level ping/pong frames to detect dropped connections, some implementation (e.g. browsers) don't provide an API for using them.

That's why Action Cable protocol has its own, protocol-level pings support.

Server sends ping messages ({ "type": "ping", "message": <Time.now.to_i>}) every X seconds (3 seconds in Rails).

Client MAY track this messages and decide to re-connect if no ping messages have been observed in the last Y seconds.

For example, default Action Cable client reconnects if no ping messages have been received in 6 seconds.

Action Cable Extended protocol

NOTE: This protocol extension is only supported by AnyCable-Go v1.4+.

The actioncable-v1-ext-json protocol adds new message types and extends the existing ones.

You can find the example implementation of the protocol in the anycable-client library.

New command: history

The new command type is added, history. It is used by the client to request historical messages for the channel. It MUST contain the identifier, command ("history"), and history fields, where history contains the history request object:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "history",
  "history": {
    "since": 1681828329,
    "streams": {
      "stream_id_1": {
        "offset": 32,
        "epoch": "x123"
      },
      "stream_id_2": {
        "offset": 54,
        "epoch": "x123"
      }
    }
  }
}

A history request contains two fields:

  • since is a UNIX timestamp in seconds indicating the time since when to fetch the history. Optional. It is used only for streams with no offset specified (usually, during the initial subscription).

  • streams is a map of stream IDs to observed offsets. Stream IDs, offsets, and epochs are received along with the messages. It's the responsibility of the client to track them and use for history requests. The epoch parameter specified the current state of the memory backend; if the current server's epoch doesn't match the requested one, the server fail to retrieve the history. For example, if in-memory backend is used to store streams history, every time a server restarts a new epoch starts.

In response to a history request, the server MUST respond with the requested historical messages (sent one by one, like during normal broadcasts, so the client shouldn't handle them specifically). Then, the server sends an acknowledgment message (confirm_history). If case messages couldn't be retrieved from the server (e.g., history has been evicted for a stream), the server MUST respond with the reject_history message.

Requesting history during subscription

It's possible to request history along with the subscribe request by adding the history field to the command payload:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "subscribe",
  "history": {
    "since": 1681828329
  }
}

Usually, in this case we can only specify the since parameter.

In response, the server MUST first confirm the subscription and then execute the history request. If the subscription is rejected, no history request is made.

New message types

Two new message types (server-client) are added:

  • [confirm_history]
  • [reject_history]

Both messages act as acknowledgments for the history command and contain the identifier key. The confirm_history message is sent to the client to indicate that the requested historical messages for the channel have been successfully sent to the client. The reject_history indicates that the server failed to retrieve the requested messages and no historical message have been sent (the client must implement a fallback mechanism to restore the consistency).

Incoming messages extensions

Broadcasted messages MAY contain metadata regarding their position in the stream. This information MUST be used with the subsequent history requests:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "message": {
    "text": "hello!",
    "user_id": 43
  },
  // NEW FIELDS:
  "stream_id": "chat_42",
  "epoch": "x123",
  "offset": 32
}

NOTE: The messages are not guaranteed to be in order (due to concurrent broadcasts), so, offsets may be non-monotonic. It's the client responsibility to keep track of offsets. Also, there is a small chance that the same message may arrive twice (from broadcast and from the history); to provide exactly-once delivery guarantees, the client MUST keep track of seen offsets and ignore duplicates.

Handshake extensions

During the handshake, the server MAY send a unique session id along with the welcome message:

{
  "type": "welcome",
  "sid": "rcl245"
}

The client MAY use this ID during re-connection to restore the session state (subscriptions, channel states, etc.) and avoid re-subscribing to the channels. For that, the previously obtained session ID must be provided either as a query parameter (?sid=rcl245) or via an HTTP header (X-ANYCABLE-RESTORE-SID).

If the server's attempt to restore the session from the sid succeeds, it MUST respond with the welcome message with the additional fields indicating that the session was restored:

{
  "type": "welcome",
  "sid": "yi421", // Note that session ID has changed
  "restored": true,
  "restored_ids": [
    "{\"channel\":\"ChatChannel\",\"id\":42}"
  ]
}

The restored flag indicates whether the session state has been restored. NOTE: In this case, no connect method is invoked at the Action Cable side.

The optional restored_ids field contains the list of channel identifiers that has been re-subscribed automatically at the server side. The client MUST NOT try to resubscribe to the specified channels and consider them connected. It's recommended to perform history requests for all the restored channels to catch up with the messages.

New command: pong

The pong command MAY be sent in response to the ping message if the server requires pongs. It could be used to improve broken connections detection.

New command: whisper new

The whisper can be used to publish broadcast messages from the client (if the whisper stream has been configured for it) to a particular channel.

The payload MUST contain command ("whisper"), identifier fields, and data fields.

The data field MAY contain a string or an object.

For example:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "whisper",
  "data": {
    "event":"typing",
    "user":"Jack"
  }
}

IMPORTANT: Unlike actions (message command), the data is not JSON-serialized. It's broadcasted to connected clients as is.