Websocket can't connect to ActionController::Live - failed with 200 - ruby-on-rails

class MessagesController < ApplicationController
include ActionController::Live
def events
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new response.stream
redis = Redis.new
redis.subscribe(redis_channel) do |on|
on.message do |event, data|
sse.write(data, event: 'messages.create')
end
end
#render nothing: true
rescue IOError
# disco bro!
ensure
redis.quit
sse.close
end
end
// also tried without the SSE.new, so just plain response.stream.write(data)
My Javascript is simple easy
var socket = new WebSocket("ws://localhost:3000/events")
socket.onmessage = function (event) {
console.log(event);
}
When i call /events in my browser and send some messages - its outputting. but if we connect the socket with JS we get
WebSocket connection to 'ws://localhost:3000/petra/events' failed:
Error during WebSocket handshake: Unexpected response code: 200
can anybody light me up? Are we missing something, or do i have a wrong understanding of what i'm trying to do.

WebSockets and Server Send Events are different protocols and can't inoperate in the manner you're trying.
The main js API for SSE is EventSource(url) (introductory article).
If you want bidirectional communication you would switch the server side to use WebSockets instead. For ruby on rails, websocket-rails is a popular library to do this.

Related

Puma webserver keeps buffering the response and sends it when calling response.stream.close

I am trying to write data to the stream at a set interval of time.
class StreamController < ApplicationController
include ActionController::Live
def stream
response.headers['Content-Type'] = 'text/event-stream'
5.times do
response.stream.write "data: hello world\n\n"
sleep 1
end
ensure
response.stream.close
end
end
But when I send a get request (using curl) to the /stream endpoint I get: "data: hello world\n\n" 5 times all at once after 5 seconds, instead of receiving "data: hello world\n\n" once per second.
I am using the default puma webserver that comes with rails 7.0.4. My guess is that it buffers the response then sends it when calling response.stream.close.
I tried adding options = { flush_headers: true } to the puma config file but it didn't help.

How to use Postman to connect to Ruby on Rails 5 Actioncable (Websocket)?

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.

Ruby server side Websocket client

Lately i have been experimenting with ruby and websockets. So i created a new Rails 5 project with ActionCable, all seems to work fine with it.
But also i created a ruby plain script with the Faye's ruby websocket client. Unlike most tutorials on internet i want to try a server side (as a client) script, not a frontend JS script inside an HTML file.
So i tried the basic usage of it and i successfully make the handshake to perform correctly but i can't continue testing because i cant figure where to subscribe after connected to a desired channel exposed in the Rails server.
Here is my ruby script code:
require 'faye/websocket'
require 'eventmachine'
EM.run {
ws = Faye::WebSocket::Client.new('ws://localhost:3001/cable',nil,{
headers: {'Origin' => 'ws://localhost:3001/cable'}
})
ws.on :open do |event|
p [:open]
ws.send({data: 'hello'})
end
ws.on :message do |event|
p [:message, event.data]
end
ws.on :close do |event|
p [:close, event.code, event.reason]
ws = nil
end
ws.on :error do |event|
p [:close, event.code, event.reason]
ws = nil
end
ws.send({data: 'yoyoyooy'}) # This gets sent to nowhere..
# I was hoping into subscribing a channel and callbacks for that channel, something like:
# ws.subscribe('my-channel',receive_message_callback,error_callback)
}
On the actioncable side my connection class does trigger the connect method, but i am still unsure how to interact with a channel. Like subscribing from the client so i can start sending and receiving messages.
By reading the readme of websocket action cable client gem i realized that any websocket would do the job, i just needed to send the proper payload in the send method according to what ActionCable needs. In this case:
ws.send {"command":"subscribe","identifier":"{\"channel\":\"#{channel}\",\"some_id\":\"1\"}"}.to_json
Off-topic: I couldn't find this in the rails overview page I just needed to search over the ActionCable Protocol

em-websocket send() send from one client to another through 2 servers

I have two websocket clients, and I want to exchange information between them.
Let's say I have two instances of socket servers, and 1st is retrieve private information, filter it and send to the second one.
require 'em-websocket'
EM.run do
EM::WebSocket.run(host: '0.0.0.0', port: 19108) do |manager_emulator|
# retrieve information. After that I need to send it to another port (9108)
end
EM::WebSocket.run(host: '0.0.0.0', port: 9108) do |fake_manager|
# I need to send filtered information here
end
end
I've tried to do something, but I got usual dark code and I don't know how to implement this functionality.
I'm not sure how you would do that using EM.
I'm assuming you will need to have the fake_manager listen to an event triggered by the manager_emulator.
It would be quite easy if you'd be using a websocket web-app framework. For instance, on the Plezi web-app framework you could write something like this:
# try the example from your terminal.
# use http://www.websocket.org/echo.html in two different browsers to observe:
#
# Window 1: http://localhost:3000/manager
# Window 2: http://localhost:3000/fake
require 'plezi'
class Manager_Controller
def on_message data
FakeManager_Controller.broadcast :_send, "Hi, fake! Please do something with: #{data}\r\n- from Manager."
true
end
def _send message
response << message
end
end
class FakeManager_Controller
def on_message data
Manager_Controller.broadcast :_send, "Hi, manager! This is yours: #{data}\r\n- from Fake."
true
end
def _send message
response << message
end
end
class HomeController
def index
"use http://www.websocket.org/echo.html in two different browsers to observe this demo in action:\r\n" +
"Window 1: http://localhost:3000/manager\r\nWindow 2: http://localhost:3000/fake\r\n"
end
end
# # optional Redis URL: automatic broadcasting across processes or machines:
# ENV['PL_REDIS_URL'] = "redis://username:password#my.host:6379"
# starts listening with default settings, on port 3000
listen
# Setup routes:
# They are automatically converted to the RESTful route: '/path/(:id)'
route '/manager', Manager_Controller
route '/fake', FakeManager_Controller
route '/', HomeController
# exit terminal to start server
exit
Good Luck!
P.S.
If you're going to keep to EM, you might consider using Redis to push and subscribe to events between the two ports.
I've found a way how to do it through em-websocket gem! You need just define variables outside of eventmachine block. Something like that
require 'em-websocket'
message_sender = nil
EM.run do
# message sender
EM::WebSocket.run(host: '0.0.0.0', port: 19108) do |ws|
ws.onopen { message_sender = ws }
ws.onclose { message_sender = nil }
end
# message receiver
EM::WebSocket.run(host: '0.0.0.0', port: 9108) do |ws|
ws.onmessage { |msg| message_sender.send(msg) if message_sender }
end
end

Avoid server stuck on SSE (Server Sent Event)

My server will get stuck, when users open SSE many times,
Because it seems Redis has some bugs with SSE.
The stream won't be closed even clients close browser or go to another page.
By the way I don't know when where the
logger.info "Stream closed"
logger.info "Client disconnected"
will be invoked ? (it doesn't be invoked when I close the browser)
Is it some workaround to avoid this issue ?
def new_prizes_stream
# http://ngauthier.com/2013/02/rails-4-sse-notify-listen.html
begin
response.headers.delete('Content-Length')
response.headers['Cache-Control'] = 'no-cache'
response.headers['Content-Type'] = 'text/event-stream'
logger.info "New stream starting, connecting to redis"
redis = Redis.new
redis.subscribe('messages.create', 'heartbeat') do |on|
on.message do |event, data|
if event == 'messages.create'
response.stream.write "event: #{event}\n"
response.stream.write "data: #{data}\n\n"
elsif event == 'heartbeat'
response.stream.write("event: heartbeat\ndata: heartbeat\n\n")
end
end
end
rescue IOError
logger.info "Stream closed"
rescue ActionController::Live::ClientDisconnected
logger.info "Client disconnected"
ensure
ap "close a live stream"
redis.quit
response.stream.close
end
end
My recommendation is that you do not create a connection on each request/SSE, and benchmark the results. Everytime you execute:
redis = Redis.new
If you can create the connection (singleton or factory patterns), instead of running this you would instead do something like:
redis = myPoolObj.getRedisConnection()
You then decide what you want to do on that Pool and how many connections you want to use. I checked the docs at redis-db but I did not see a built-in API for this like I saw when inspecting the python one.
You can open an issue on the repo asking if there's a built-in way to execute this without managing your own pool.
Did this help?

Resources