What can be the reason of “Unable to find subscription with identifier” in Rails ActionCable?

喜欢而已 提交于 2019-12-04 08:37:20

It looks like it's related to this issue: https://github.com/rails/rails/issues/25381

Some kind of race conditions when Rails reply the subscription has been created but in fact it hasn't been done yet.

As a temporary solution adding a small timeout after establishing the subscription has solved the issue.

More investigation needs to be done, though.

The reason for this error might be the difference of the identifiers you subscribe to and messaging to. I use ActionCable in Rails 5 API mode (with gem 'devise_token_auth') and I faced the same error too:

SUBSCRIBE (ERROR):

{"command":"subscribe","identifier":"{\"channel\":\"UnreadChannel\"}"}

SEND MESSAGE (ERROR):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\",\"correspondent\":\"client2@example.com\"}","data":"{\"action\":\"process_unread_on_server\"}"}

For some reason ActionCable requires your client instance to apply the same identifier twice - while subscribing and while messaging:

/var/lib/gems/2.3.0/gems/actioncable-5.0.1/lib/action_cable/connection/subscriptions.rb:74

def find(data)
  if subscription = subscriptions[data['identifier']]
    subscription
  else
    raise "Unable to find subscription with identifier: #{data['identifier']}"
  end
end

This is a live example: I implement a messaging subsystem where users get the unread messages notifications in the real-time mode. At the time of the subscription, I don't really need a correspondent, but at the messaging time - I do.

So the solution is to move the correspondent from identifier hash to the data hash:

SEND MESSAGE (CORRECT):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\"}","data":"{\"correspondent\":\"client2@example.com\",\"action\":\"process_unread_on_server\"}"}

This way the error is gone.

Here's my UnreadChannel code:

class UnreadChannel < ApplicationCable::Channel
  def subscribed

    if current_user

      unread_chanel_token = signed_token current_user.email

      stream_from "unread_#{unread_chanel_token}_channel"

    else
# http://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#class-ActionCable::Channel::Base-label-Rejecting+subscription+requests
      reject

    end

  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def process_unread_on_server param_message

    correspondent = param_message["correspondent"]

    correspondent_user = User.find_by email: correspondent

    if correspondent_user

      unread_chanel_token = signed_token correspondent

      ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel",
                                   sender_id: current_user.id
    end

  end

end

helper: (you shouldn't expose plain identifiers - encode them the same way Rails encodes plain cookies to signed ones)

  def signed_token string1

    token = string1

# http://vesavanska.com/2013/signing-and-encrypting-data-with-tools-built-in-to-rails

    secret_key_base = Rails.application.secrets.secret_key_base

    verifier = ActiveSupport::MessageVerifier.new secret_key_base

    signed_token1 = verifier.generate token

    pos = signed_token1.index('--') + 2

    signed_token1.slice pos..-1

  end  

To summarize it all you must first call SUBSCRIBE command if you want later call MESSAGE command. Both commands must have the same identifier hash (here "channel"). What is interesting here, the subscribed hook is not required (!) - even without it you can still send messages (after SUBSCRIBE) (but nobody would receive them - without the subscribed hook).

Another interesting point here is that inside the subscribed hook I use this code:

stream_from "unread_#{unread_chanel_token}_channel"

and obviously the unread_chanel_token could be whatever - it applies only to the "receiving" direction.

So the subscription identifier (like \"channel\":\"UnreadChannel\") has to be considered as a "password" for the future message-sending operations (e.g. it applies only to the "sending" direction) - if you want to send a message, (first send subscribe, and then) provide the same "pass" again, or you'll get the described error.

And more of it - it's really just a "password" - as you can see, you can actually send a message to whereever you want:

ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel", sender_id: current_user.id

Weird, right?

This all is pretty complicated. Why is it not described in the official documentation?

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!