Add Action Cable to Rails 4.2 App with DeviseJanuary 25, 2016
I needed to integrate Action Cable into a Rails 4.2 app with Devise and I figured I'd document what worked for me. Strangely, none of the example repos on Github worked out of the box but following this tutorial with a few changes was the best place to start and creating a Warden hook as specified here proved to be the easiest way to get Devise working as well. Here are the steps I took and some of the pitfalls I encountered. We'll be adding an arbitrary chat component to a Rails 4 app where a user can only talk to themselves. Admittedly not a super useful feature but it will demonstrate how a channel can be scoped to a specific user, which is where I experienced the most issues.
Start by adding Action Cable and Puma (if you aren't already using it) to your Gemfile. Now that Action Cable has been integrated into Rails you have to point your Gemfile to the 'archive' branch:
gem 'actioncable', github: 'rails/actioncable', branch: 'archive'
gem 'puma'
Next add the message routes:
resources :messages, only: [:index, :create]
We have to use Warden hooks to create the signed cookies that the Action Cable server will use to find the current user. So create a new warden_hooks.rb initializer or add them to the Devise initializer if you prefer:
# app/config/initializers/warden_hooks.rb
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
endWarden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil
end
This uses hooks in the Devise authentication lifecycle to set and expire signed cookies.
Next add a messages controller:
class MessagesController < ApplicationController
before_filter :authenticate_user!
protect_from_forgery :except => :createdef create
ActionCable.server.broadcast "messages_#{current_user.id}",
message: params[:message][:body],
username: cookies.signed['user.id']
head :ok
end
end
Devise has an issue with CSRF tokens when making AJAX calls and there are better ways to deal with this than disabling the protection but our current focus is on Action Cable so I'm going to keep it simple.
Next we'll add our messages template:
<h2>Messages</h2>
<br/><br/>
<div id='messages'></div>
<br/><br/>
<%= form_for :message, url: messages_path, remote: true, id: 'messages-form' do |f| %>
<%= f.label :body, 'Enter a message:' %><br/>
<%= f.text_field :body %><br/>
<%= f.submit 'Send message' %>
<% end %>
Next we need to add two additional classes ApplicationCable::Connection which is code that gets executed whenever a client connects to the Cable server, and ApplicationCable::Channel, which acts as the parent class to all channels in case you have a need for shared logic:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_userdef connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.name
endprotected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now
verified_user
else
reject_unauthorized_connection
end
end
end
end# app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
Obviously if your User model doesn't have a 'name' attribute you'd want to replace that logger.add_tags parameter.
Action Cable relies on Redis' PubSub feature so you need to have Redis installed and you need to add a config file (config/redis/cable.yml):
local: &local
:url: redis://localhost:6379
:host: localhost
:port: 6379
:timeout: 1
:inline: true
development: *local
test: *local
Though apparently you can integrate Action Cable directly into your existing app process, we are setting it up as a separate server so we need to create a bin/cable executable and a rackup file:
# /bin/cable
bundle exec puma -p 28080 cable/config.ru
To make this file executable you generally have to change permissions. From your app's root directory:
chmod 711 bin/cable
# cable/config.ru
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!require 'action_cable/process/logging'
run ActionCable.server
We then need to add our MessagesChannel:
# app/channels/messages_channel.rb
class MessagesChannel < ApplicationCable::Channel
def subscribed
stream_from "messages_#{current_user.id}"
end
end
This subscribed method gets called whenever a client subscribes to a channel and "messages_#{current_user.id}" provides each user with their own channel. If you just left this line as stream_from 'messages' all users would be subscribed to the same channel.
Next we need to set things up on the client side. First create a app/assets/javascripts/application_cable.coffee file and add the code:
#= require cable@App = {}
App.cable = Cable.createConsumer("ws://localhost:28080")
This gives the Cable server access to the signed cookies, and was where I had the most issues. Various tutorials left the WebSocket address pointing to example domains or used "ws://127.0.0.1:28080" but in my experience the only thing that worked was pointing it to "ws://localhost:28080".
Then we create the file app/assets/javascripts/channels/messages.coffee and add the code that subscribes the client to the messages channel and sets up how to handle incoming data:
App.messages = App.cable.subscriptions.create 'MessagesChannel',
received: (data) ->
$('#messages').append @renderMessage(data)renderMessage: (data) ->
"<p><b>[#{data.username}]:</b> #{data.message}</p>"
And that should be it!
Open up two terminal windows and navigate to your application's root directory in both. Start the Cable server in one by running:
bin/cableand in the second fire up your rails server:
bundle exec rails sOpen up two browser windows, log in on both of them and visit localhost:3000/messages. When you send a message you should see your id and your message come up simultaneously in both browser windows. Hooray!
By far the biggest issues I faced related to signed cookies and authentication. But now that I've got it authenticating correctly with Devise, and scoping correctly to the user, I'm looking forward to creating some more useful channels.
Sometimes when working with Action Cable you'll do something (like submit a form) and nothing at all will happen, no errors in your Rails server or your Cable server and no JavaScript errors either. You can use the redis-cli to monitor interactions with Redis pub sub to help monitor issues:
$> redis-cli
$> monitor
if you get an error in the console stating that "websocket-is-closed-before-the-connection-is-established" make sure you don't have a firewall that is interfering with the connection.
Again, credit and many thanks to Nithin Bekal for his post that was the basis for this one and the only tutorial that worked for me by default. Also thanks to Greg Molnar for his post on integrating Devise.