Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 5 ActionCable WebSockets is not returning upgrade headers with status 101 Upgrade response

EDIT: Shown at end, found that upgrade headers were actually created.

I'm working from the action-cable-example codebase, trying to build a WebSocket app. The "Chatty" application, which depends upon the browser client provided in the app, works fine. But, I am not going to use that client as I need an external IoT connection. As a result, I am trying to implement the ws/wss WebSocket protocols to external non-browser devices and my connection in route.rb is:

mount ActionCable.server => '/cable'

I've tried several external clients, such as the Chrome Simple WebSocket Client extension and gem websocket-client-simple using sample/client.rb. In both cases, ActionCable returns no upgrade headers. The Chrome Extension complains as follows:

WebSocket connection to 'ws://127.0.0.1:3000/cable' failed: Error during WebSocket handshake: 'Upgrade' header is missing 

The actual handshake shows that to be true, as in:

**General**
Request URL:ws://127.0.0.1:3000/cable
Request Method:GET
Status Code:101 Switching Protocols
**Response Headers**
view source
Connection:keep-alive
Server:thin
**Request Headers**
view source
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:Upgrade
Cookie:PPA_ID=<redacted>
DNT:1
Host:127.0.0.1:3000
Origin:chrome-extension://pfdhoblngboilpfeibdedpjgfnlcodoo
Pragma:no-cache
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:1vokmzewcWf9e2RwMth0Lw==
Sec-WebSocket-Version:13
Upgrade:websocket
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36

Per the standards, the response headers are to be this:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

The Sec-WebSocket-Accept is particularly important, as it's a calculation based on the request header's Sec-WebSocket-Key to confirm that ws/wss is understood and that the the Switching Protocols should occur.

During all of this, the server is more happy, until the client gets ticked and closes the connection:

Started GET "/cable" for 127.0.0.1 at 2016-06-16 19:19:17 -0400
  ActiveRecord::SchemaMigration Load (1.0ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2016-06-16 19:19:17 -0400
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
Finished "/cable/" [WebSocket] for 127.0.0.1 at 2016-06-16 19:19:18 -0400

Looking at websocket-client-simple, I broke down the WebSocket returned to client.rb, and it also showed empty headers. I am showing the code and then the WebSocket:

url = ARGV.shift || 'ws://localhost:3000/cable'
ws = WebSocket::Client::Simple.connect url

#<WebSocket::Client::Simple::Client:0x2cdaf68 
    @url="ws://localhost:3000/cable", 
    @socket=#<TCPSocket:fd 3>, 
    @handshake=<WebSocket::Handshake::Client:0x013231c8 
        @url="ws://localhost:3000/cable", 
        @headers={}, 
        @state=:new, 
        @handler=#<WebSocket::Handshake::Handler::Client11:0x2e88400 
            @handshake=<WebSocket::Handshake::Client:0x013231c8 
                @url="ws://localhost:3000/cable", 
                @headers={}, 
                @state=:new, 
                @handler=#<WebSocket::Handshake::Handler::Client11:0x2e88400 ...>, 
                @data="", 
                @secure=false, 
                @host="localhost", 
                @port=3000, 
                @path="/cable", 
                @query=nil, 
                @version=13>, 
            @key="KUJ0/C0rvoCMruW8STp0Sw==">, 
        @data="", 
        @secure=false, 
        @host="localhost", 
        @port=3000, 
        @path="/cable", 
        @query=nil, 
        @version=13>, 
    @handshaked=false, 
    @pipe_broken=false, 
    @closed=false, 
    @__events=[{:type=>:__close, :listener=>#<Proc:0x2d10ae8@D:/Bitnami/rubystack-2.2.5-3/projects/websocket-client-simple/lib/websocket-client-simple/client.rb:37>, :params=>{:once=>true}, :id=>0}], 
    @thread=#<Thread:0x2d10a70@D:/Bitnami/rubystack-2.2.5-3/projects/websocket-client-simple/lib/websocket-client-simple/client.rb:42 sleep>
>;

In this response, I noted the instance variable "@handshaked" is returned as false. That may be relevant, but I haven't found where that is set or referenced within the code so far.

UPDATE: Found that WebSocket::Driver.start actually creates the upgrade headers. And, @socket.write(response) should send them out through EventMachine. Code:

def start
  return false unless @ready_state == 0
  response = handshake_response
  return false unless response
  @socket.write(response)
  open unless @stage == -1
  true
end

handshake_response is:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: iJVnsG1ApNMFzABXGDSHN1V0i/s=
like image 357
Richard_G Avatar asked Jan 06 '23 21:01

Richard_G


1 Answers

The problem was that I was trying to use the Thin server in development. It would operate. However, it was actually transmitting the response headers during its processing, such as this:

Response Headers
Connection:keep-alive
Server:thin

ActionCable was actually sending the appropriate upgrade headers, but it was doing so only after Thin had sent out its own headers so the client didn't recognize them.

After converting back to Puma, I receive these as expected:

Response Headers
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: XJOmp1e2IwQIMk5n0JV/RZZSIhs=
like image 179
Richard_G Avatar answered Jan 31 '23 08:01

Richard_G