I am working in a project which has 3 separate parts - a ruby on rails engine, a ruby on rails application, and a frontend react application. I need to introduce websockets / ActionCable in the engine, and am not having any luck getting ActionCable running.
I have the following relevant files in the engine:
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
app/channels/fromserver_channel.rb
class FromServerChannel < ApplicationCable::Channel
def subscribed
stream_from "fromClient"
end
def receive(data)
puts '----------'
puts data
ActionCable.server.broadcast "fromServer", data
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
config/environments/development.rb
require 'action_cable/engine'
Rails.application.configure do
config.action_cable.disable_request_forgery_protection = true
#specify the socket URI for ActionCable
config.action_cable.url = "/cable"
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
end
config/cable.rb
development:
adapter: async
test:
adapter: async
production:
adapter: postgresql
config/routes.rb
require 'action_cable/engine'
RoundaboutEngine::Engine.routes.draw do
mount ActionCable.server => '/cable'
# mount RoundaboutEngine::Engine => "/cable"
scope controller: 'round_about' do
get 'organizer' => :show
end
end
Rails.application.routes.send(:draw) do
mount RoundaboutEngine::Engine => '/' if RoundaboutEngine.draw_routes_in_host_app
end
The front end looks like this:
import React, { useState } from 'react';
import channel from './cable';
const RoundAboutView = () => {
const [displayText, setDisplayText] = useState('');
const sendUpdate = (arg) => {
channel.sending(arg);
};
return (
...
Text:
{` ${displayText}`}
...
);
};
export default RoundAboutView;
import ActionCable from 'actioncable';
// const cable = ActionCable.createConsumer('wss://localhost:9292/cable');
const cable = ActionCable.createConsumer(`ws://${window.location.host}/cable`);
const channel = cable.subscriptions.create(
{ channel: 'FromServerChannel' },
{
connected() { },
disconnected() {},
received: (data) => {
console.log(data);
},
sending(data) {
this.perform('fromClient', { data });
},
},
);
export default channel;
The problem I am having is that Chrome says the websocket can't connect (WebSocket connection to 'ws://localhost:9292/cable' failed). In the terminal, when in routes.rb I mount /cable with ActionCable.server, I see the below:
"exception":{
"type":"NoMethodError",
"message":"undefined method `info' for nil:NilClass",
"stack_trace":"NoMethodError(undefined method `info' for nil:NilClass)\n actioncable (5.2.6) lib/action_cable/connection/tagged_logger_proxy.rb:38:in `block in log'\n actioncable (5.2.6)
When I don't do that and instead mount /cable with the engine, I get an error that the route GET /cable doesn't exist.
I'm not able to move past this point and I'm not sure what I'm doing wrong or missing in the code. I've seen the example at https://github.com/palkan/engine-cable-app and wasn't able to take that example and make it work in my code. Can anyone help me?
Related
I am running a Rails 5.2.7 engine which has actioncable (websockets) set up. The websocket connection is successfully established when the application is run via the browser, using a React UI with actioncable-js-jwt, a version of actioncable's js package that supports jwt. However I am unable to establish the connection via postman's beta websockets option.
My connection.rb file is very simple:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
# Public: Invoked as part of the websocket connection establishment
def connect
self.current_user = {
...(hardcoded values)
}
end
end
end
Engine.rb looks like this:
require 'action_cable/engine'
module RoundaboutEngine
class << self
def cable
#cable ||= ActionCable::Server::Configuration.new
end
end
class Engine < ::Rails::Engine
isolate_namespace RoundaboutEngine
class << self
def server
#server ||= ActionCable::Server::Base.new(config: RoundaboutEngine.cable)
end
end
config.roundaboutengine_cable = RoundaboutEngine.cable
config.roundaboutengine_cable.mount_path = '/cable'
config.roundaboutengine_cable.connection_class = -> { RoundaboutEngine::Connection }
config.roundaboutengine_cable.disable_request_forgery_protection = true
initializer 'roundaboutengine_cable.cable.config' do
RoundaboutEngine.cable.cable = { adapter: 'async' }
end
initializer 'roundaboutengine_cable.cable.logger' do
RoundaboutEngine.cable.logger ||= ::Rails.logger
end
end
end
The browser request network tab shows the following:
Request URL: ws://localhost:9292/cable
Request Method: GET
Status Code: 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: [string]
Sec-WebSocket-Protocol: actioncable-v1-json
Upgrade: websocket
In postman, I also use ws://localhost:9292/cable as a raw connection. However, it seems to turn that into an http url, as shown in the request information:
Request URL: http://localhost:9292/cable
Request Method: GET
Status Code: 404 Not Found
Request Headers
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: [string]
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Host: localhost:9292
I am not sure why postman is turning the request into an http request, or how to make this work.
I also tried adding Sec-WebSocket-Protocol: actioncable-v1-json to the postman request headers, with no luck.
Resolved: I had to replace a line in engine.rb
I removed:
config.roundaboutengine_cable.disable_request_forgery_protection = true
I added:
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/, nil]
In particular, the nil was important for postman and is what was causing the problem.
I'm currently using Rails 6 and Linux mint Cinnamon 20.2, and I have problems with Redis configuration / setupping in my Rails application.
My problem is that my ActionCable doesn't work well.
I would like to build a real time chat app, I created a channel, called "Room".
Here how my room_channel.js file looks like:
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "RoomChannel", room_id: 3 }, {
connected() {
console.log('Connected successfully!')
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log(data)
}
});
Here's my room_channel.rb:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params[:room_id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Here's the important segment of my messages_controller.rb file (The ActionCable command is the important):
def create
#message = Message.new(message_params)
#message.user = User.generate
#message.save
ActionCable.server.broadcast("room_channel_#{#message.room_id}",{message:"hello"})
end
When I join to Room 3 (3 is the ID of that room), I get my "Connected successfully" message, but when I send a message, it doesn't output "hello" in the console. It means it doesn't receive successfully datas, right?
What can i do with Redis to be able to receive data?
Finally, I changed the development section of my config/cable.yml file to, but it doesn't solved anything :(.
development:
adapter: redis
url: redis://localhost:6379/1
Anybody can help me out with this?
Thanks for your help!
A vanilla Rails 6 application is giving this error:
"WebSocket connection to 'ws://localhost:3000/cable' failed: WebSocket is closed due to suspension"
Actioncable Channel:
#app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log(data)
// Called when there's incoming data on the websocket for this channel
}
});
Rails Controller:
#app/controller/messages_controller:
puts "*******Broadcasting"
ActionCable.server.broadcast("chat_channel", content: #message.id)
ActionCable Channel:
# app/channele/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
puts "**** ChatChannel Subscribed"
stream_from "chat_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Where did I slip up?
Make sure in your routes.rb has this mount ActionCable.server => '/cable'
And in your development.rb file
config.action_cable.url = "ws://localhost:3000/cable"
config.action_cable.allowed_request_origins = [/http:\/\/*/,
/https:\/\/*/]
I am developing now Streaming API with Rails5 api mode, for React website.
That use it in notification functions.
I'm in below's env:
<server side>
rails: v5.1.5
ruby: 2.4.2
<client side>
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-router": "^2.7.0",
"actioncable": "^5.1.5",
Server side code:
app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
development.rb
config.action_cable.url = "ws://localhost:3000/cable"
application.rb
# websocket
config.action_cable.allowed_request_origins = [
# Local address of our RoR server
'http://localhost:3000',
# Local address we use for our React standalone client
'http://localhost:8000',
'chrome-extension://pfdhoblngboilpfeibdedpjgfnlcodoo'
]
ActionCable.server.config.disable_request_forgery_protection = true
routes.rb
mount ActionCable.server => '/cable'
xxx.rb
ActionCable.server.broadcast('notifications', notification: #notification)
Client side:
componentDidMount () {
this.createSocket()
}
createSocket = () => {
let App = {};
App.cable = ActionCable.createConsumer('ws://localhost:3000/cable');
let subscription = App.cable.subscriptions.create({channel:'NotificationsChannel'}, {
connected : () => {},
disconnected : () => {},
received : (data) => {
console.log(data)
}
});
console.log(subscription)
};
And so, this is server side console
Started GET "/cable" for 127.0.0.1 at 2018-03-02 13:37:30 +0800
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2018-03-02 13:37:30 +0800
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION:Upgrade, HTTP_UPGRADE: websocket)
NotificationsChannel is transmitting the subscription confirmation
NotificationsChannel is streaming from notifications
I am referring this website https://www.npmjs.com/package/actioncable and https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable this one.
BUT the data is not displayed in client console.
How do I change code?
Thanks to #GorillaApe
I solved this problem.
It is by setting about cable.yml .
aysnc doesn't work in this case. redis is better as adapter.
this is my cable.yml now:
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: redis
url: redis://localhost:6379
production:
adapter: redis
url: redis://localhost:6379
channel_prefix: api_notification_production
I currently have a Rails 5 application acting as my backend, we can call this the "Core." I also have another Rails 5 application acting as my frontend, which is serving up AngularJS client side, we can call this the "Front". These are two completely separate Rails 5 applications with completely different domains.
Basically, I am trying to integrate Action Cable through the Core and have it talk to the Front. I'm using this service here for the Front: https://www.npmjs.com/package/angular-actioncable. As far as the Core, that's just basic Action Cable set up.
Problem: I'm having some trouble getting the handshake to work across two separate domains. I've read through everything I can find online, unfortunately there is not much information. If you've done this before, please help!
Note: I do have Redis server running and I'm using separate ports to mimic the separate domains in development.
Core:
chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from 'http://localhost:2000/#/chat'
end
def unsubscribed
stop_all_streams
end
def receive(data)
ActionCable.server.broadcast('http://localhost:2000/#/chat', data)
end
def speak
params.fetch('data').fetch('chat')
end
end
route.js
mount ActionCable.server => '/cable'
cable.yml
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379
config/environments.rb
config.action_cable.disable_request_forgery_protection = true
Front:
ChatCtrl.js
app.controller('ChatCtrl', ['$scope', 'ActionCableChannel',
function($scope, ActionCableChannel) {
$scope.inputText;
$scope.chatData = [];
var consumer = new ActionCableChannel("ChatChannel");
var callback = function(message) {
$scope.chatData.push(message);
};
consumer.subscribe(callback).then(function() {
$scope.sendToMyChannel = function(message) {
consumer.send(message, 'speak');
};
$scope.$on("$destroy", function() {
consumer.unsubscribe().then(function() {
$scope.sendToMyChannel = undefined;
});
});
});
}
]);
// Action Cable Configuration
app.run(function (ActionCableConfig) {
ActionCableConfig.wsUri = 'localhost:4000';
});
Error Message in Console:
Try
ActionCableConfig.wsUri = 'ws://localhost:4000';