20

I'm trying to create a WS connection with my tornado server. The server code is simple:

class WebSocketHandler(tornado.websocket.WebSocketHandler):

    def open(self):
        print("WebSocket opened")

    def on_message(self, message):
        self.write_message(u"You said: " + message)

    def on_close(self):
        print("WebSocket closed")

def main():

    settings = {
        "static_path": os.path.join(os.path.dirname(__file__), "static")
    }


    app = tornado.web.Application([
            (r'/ws', WebSocketHandler),
            (r"/()$", tornado.web.StaticFileHandler, {'path':'static/index.html'}),
        ], **settings)


    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

I copy pasted the client code from here:

$(document).ready(function () {
    if ("WebSocket" in window) {

        console.log('WebSocket is supported by your browser.');

        var serviceUrl = 'ws://localhost:8888/ws';
        var protocol = 'Chat-1.0';
        var socket = new WebSocket(serviceUrl, protocol);

        socket.onopen = function () {
            console.log('Connection Established!');
        };

        socket.onclose = function () {
            console.log('Connection Closed!');
        };

        socket.onerror = function (error) {
            console.log('Error Occured: ' + error);
        };

        socket.onmessage = function (e) {
            if (typeof e.data === "string") {
                console.log('String message received: ' + e.data);
            }
            else if (e.data instanceof ArrayBuffer) {
                console.log('ArrayBuffer received: ' + e.data);
            }
            else if (e.data instanceof Blob) {
                console.log('Blob received: ' + e.data);
            }
        };

        socket.send("Hello WebSocket!");
        socket.close();
    }
});

When it tries to connect i get the following output on the browser's console:

WebSocket connection to 'ws://localhost:8888/ws' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received

Why is that?

2
  • Post your client connection code, or we'll be guessing... Commented Dec 10, 2015 at 11:31
  • I used the Chat-1 protocol. Eventually I deleted that part, and opened the WS without specifying the protocol and works this way. I'm still interested how I should configure the server side to accept it. Commented Dec 10, 2015 at 11:34

2 Answers 2

30

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: true

class 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}"
  end
end

module 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 self
end

# config.ru
if __FILE__.end_with? ".ru"
  run APP 
else
# terminal?
  require 'iodine'
  Iodine.threads = 1
  Iodine.listen2http app: APP, log: true
  Iodine.start
end

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!"); };
Sign up to request clarification or add additional context in comments.

5 Comments

I would be very interested in a Ruby example. I am playing around with the wamp gem and have the same issue (github.com/bradylove/wamp-ruby) but it uses v1. I am thinking of writing a v2 WAMP gem ...
@awenkhh - I'm not sure I understand your issue. This is a client related issue and the gem you mentioned is a server implementation. Maybe you could open a new question with a full trace and leave a link back here?
Hey, may I have Ruby example if you still have it?
@alt-ja , I edited the answer to add a Ruby example for your convenience. Good luck!
For me the fix was indeed to remove the subprotocol name (second param) in the constructor.
1

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::Integration
Properties:
  ApiId: !Ref MyAPI
  IntegrationType: AWS_PROXY
  IntegrationUri: !GetAtt MyLambdaFunction.Arn
  IntegrationMethod: POST
  ConnectionType: INTERNET 

The most important are the last two lines.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.