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=
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=
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With