WebSocket: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
As pointed out in whatwg.org's Websocket documentation (it's a copy from the standard's draft):
The WebSocket(url, protocols) constructor takes one or two arguments. The first argument, url, specifies the URL to which to connect. The second, protocols, if present, is either a string or an array of strings. If it is a string, it is equivalent to an array consisting of just that string; if it is omitted, it is equivalent to the empty array. Each string in the array is a subprotocol name. The connection will only be established if the server reports that it has selected one of these subprotocols. The subprotocol names must all be strings that match the requirements for elements that comprise the value of Sec-WebSocket-Protocol fields as defined by the WebSocket protocol specification.
Your server answers the websocket connection request with an empty Sec-WebSocket-Protocol
header, since it doesn't support the Chat-1
subprotocol.
Since you're writing both the server side and the client side (and unless your writing an API you intend to share), it shouldn't be super important to set a specific subprotocol name.
You can fix this by either removing the subprotocol name from the javascript connection:
var socket = new WebSocket(serviceUrl);
Or by modifying your server to support the protocol requested.
I could give a Ruby example, but I can't give a Python example since I don't have enough information.
EDIT (Ruby example)
Since I was asked in the comments, here's a Ruby example.
This example requires the iodine
HTTP/WebSockets server, since it supports the rack.upgrade
specification draft (concept detailed here) and adds a pub/sub API.
The server code can be either executed through the terminal or as a Rack application in a config.ru
file (run iodine
from the command line to start the server):
# frozen_string_literal: trueclass ChatClient def on_open client @nickname = client.env['PATH_INFO'].to_s.split('/')[1] || "Guest" client.subscribe :chat client.publish :chat , "#{@nickname} joined the chat." if client.env['my_websocket.protocol'] client.write "You're using the #{client.env['my_websocket.protocol']} protocol" else client.write "You're not using a protocol, but we let it slide" end end def on_close client client.publish :chat , "#{@nickname} left the chat." end def on_message client, message client.publish :chat , "#{@nickname}: #{message}" endendmodule APP # the Rack application def self.call env return [200, {}, ["Hello World"]] unless env["rack.upgrade?"] env["rack.upgrade"] = ChatClient.new protocol = select_protocol(env) if protocol # we will use the same client for all protocols, because it's a toy example env['my_websocket.protocol'] = protocol # <= used by the client [101, { "Sec-Websocket-Protocol" => protocol }, []] else # we can either refuse the connection, or allow it without a match # here, it is allowed [101, {}, []] end end # the allowed protocols PROTOCOLS = %w{ chat-1.0 soap raw } def select_protocol(env) request_protocols = env["HTTP_SEC_WEBSOCKET_PROTOCOL"] unless request_protocols.nil? request_protocols = request_protocols.split(/,\s?/) if request_protocols.is_a?(String) request_protocols.detect { |request_protocol| PROTOCOLS.include? request_protocol } end # either `nil` or the result of `request_protocols.detect` are returned end # make functions available as a singleton module extend selfend# config.ruif __FILE__.end_with? ".ru" run APP else# terminal? require 'iodine' Iodine.threads = 1 Iodine.listen2http app: APP, log: true Iodine.startend
To test the code, the following JavaScript should work:
ws = new WebSocket("ws://localhost:3000/Mitchel", "chat-1.0");ws.onmessage = function(e) { console.log(e.data); };ws.onclose = function(e) { console.log("Closed"); };ws.onopen = function(e) { e.target.send("Yo!"); };
For those who use cloudformation templates, AWS has a nice example here.
UPDATE
The key thing is the response in the connection function. On the abovementioned AWS shows how this can be done:
exports.handler = async (event) => { if (event.headers != undefined) { const headers = toLowerCaseProperties(event.headers); if (headers['sec-websocket-protocol'] != undefined) { const subprotocolHeader = headers['sec-websocket-protocol']; const subprotocols = subprotocolHeader.split(','); if (subprotocols.indexOf('myprotocol') >= 0) { const response = { statusCode: 200, headers: { "Sec-WebSocket-Protocol" : "myprotocol" } }; return response; } } } const response = { statusCode: 400 }; return response;};function toLowerCaseProperties(obj) { var wrapper = {}; for (var key in obj) { wrapper[key.toLowerCase()] = obj[key]; } return wrapper;}
Please note the header settings in the response. Also this response must be delivered to the requester, for this response integration must be configured.
In the AWS example consider the code:
MyIntegration:Type: AWS::ApiGatewayV2::IntegrationProperties: ApiId: !Ref MyAPI IntegrationType: AWS_PROXY IntegrationUri: !GetAtt MyLambdaFunction.Arn IntegrationMethod: POST ConnectionType: INTERNET
The most important are the last two lines.