Bad Handshake issue when using Secure Web Sockets

Hi, I’m using K6 (.25 version) to perform load testing on a WSS url. I’m getting the Bad Handshake issue and I coudn’t find much information on whether K6 supports Secure Web Sockets (WSS). Can you please help if there is any workaround to make K6 support WSS

INFO[0003] An unexpected error occured: 0=“websocket: bad handshake”
ERRO[0003] GoError: websocket: bad handshake-
at native

Thanks in advance.

Regards,
Vinay

Hi,

k6 does support WSS. The example found here also works if you use the WSS endpoint (wss://echo.websocket.org).

Looking at the source code of the WS library used by k6, this error is returned if the server returns an invalid response to an Upgrade request. So make sure that the server you’re testing is sending a compliant response.

Thanks much Ivan for the reply.

I tried with the very same example. I’m able to successfully connect with the Chrome Web Socket Client plugin.

How can we enable some debugging on this to know the request and response from the server?

k6 has an --http-debug option, but unfortunately no equivalent for WebSockets. See this FAQ for details.

You could attempt --http-debug with a manually crafted Upgrade request like so:

import http from "k6/http";

export default function () {
   http.get("http://echo.websocket.org", {
       headers: {
           "Connection": "Upgrade",
           "Upgrade": "websocket",
           "Sec-WebSocket-Key": "1WKeF1XWzkUXPHzXiYAKCg==",
       }
   });
}

… but I wasn’t able to get a valid 101 response, so YMMV.

Alternatively, you can use a TCP/HTTP tracing tool like tcpdump, Wireshark or Sysdig to record and visualize the request/response.

I’m able to make it work by explicitly sending the ‘Origin’ field in header ,

Is it something K6 can add in future releases?

Regards,
Vinay

Glad that you got it working.

Setting the ‘Origin’ header would be outside of k6 concerns, since it’s application-specific, and you can set it manually, as you’ve seen.

I’m getting this trying to connect to a SignalR connection. I’ve set all the headers I can see from Chrome developer tools but still get this error.

Some debugging would help.

PS Without SSL works locally to my own PC, attempts to connect to remote servers seems to be the issue

OK, for SignalR with cookie authentication. I had to set the Cookie using a header. The cookie field in the parameters object doesn’t seem to be effective.

It was working locally because my local iis allowed localhost connections unauthenticated.

@woakesd yeah, sorry, this is a known bug :disappointed: Support passing CookieJar to WebSocket through Params · Issue #1226 · grafana/k6 · GitHub

I’m sending ping every second and getting the pong, but after around 15 seconds I’m getting the message

{“error”:“Handshake was canceled.”}

The close handler is then called.

Looking at the messages in Chrome, the server is ending messages periodically (the interval is variable, but typically less than 60s) which look like:

{“type”: 6}

Actual messages look like:

{“type”:1,“target”:“ReceiveMessage”,“arguments”:[ … ]}

The socket message handler isn’t firing for either of these…

@woakesd Can you share the output of k6 version, your test script or relevant parts of it, and if there’s a public test instance of the WS server you’re testing against or if there’s anything otherwise peculiar about its implementation?

We need to be able to reproduce your issue to help, and determine whether this is a bug in k6 and its details if so.

I can’t point you at the real site unfortunately but will try to set up something which demonstrates this.

Script:

         var res = http.post(`${urlBase}/MatchDispatcher/activitieshub/negotiate`, null, {
                "responseType": "text"
            }
        );

        var connectionId = res.json()["connectionId"];
        console.log(`Connection Id is ${connectionId}`);

        var url = `${webSocketBase}/MatchDispatcher/activitieshub?id=${connectionId}`;
        console.log(url);

        var response = ws.connect(url, {
                headers: {
                    "Cookie": `authCookieName=${authCookie}`,
                    "Origin": `https://${site}"`
                }
            }, function (socket) {
                socket.on('open', function open() {
                    console.log('connected');
                  });
            
                socket.on('message', function (message) {
                    console.log(`Received message: ${message}`);
                });
            
                socket.on('close', function () {
                    console.log('disconnected');
                });
            
                socket.on('error', function (e) {
                    if (e.error() != "websocket: close sent") {
                        console.log('An unexpected error occurred: ', e.error());
                    }
                });
            
                socket.setTimeout(function () {
                    console.log('60 seconds passed, closing the socket');
                    socket.close();
                }, 60000);
            });
        
            check(response, { "status is 101": (r) => r && r.status === 101 });
            response.close();
        }

Output from K6

Request:
POST /MatchDispatcher/activitieshub/negotiate HTTP/1.1
Host: SSSSSSS
User-Agent: k6/0.25.1 (https://k6.io/)
Content-Length: 0
Cookie: .AspNet.SMAIdentity=XXXXXXXXXXXX
Accept-Encoding: gzip


Response:
HTTP/1.1 200 OK
Content-Length: 252
Content-Type: application/json
Date: Wed, 18 Dec 2019 12:23:34 GMT
Server: Microsoft-IIS/8.5
X-Powered-By: ASP.NET
X-Powered-By: ARR/3.0
X-Powered-By: ASP.NET


INFO[0003] Connection Id is CCCCCID
INFO[0003] wss://SSSSSSS/MatchDispatcher/activitieshub?id=CCCCCID
INFO[0003] connected
INFO[0018] Received message: {"error":"Handshake was canceled."}
INFO[0018] disconnected
INFO[0063] 60 seconds passed, closing the socket
ERRO[0063] TypeError: Cannot read property 'error' of undefined
        at file:///C:/Users/P10306702/source/repos/test/load-impact-assessment/test-alert-monitor.js:107:9(3)
        at native
        at file:///C:/Users/P10306702/source/repos/test/load-impact-assessment/test-alert-monitor.js:114:5(9)
        at native
        at file:///C:/Users/P10306702/source/repos/test/load-impact-assessment/test-alert-monitor.js:91:15(62)
        at native
        at file:///C:/Users/P10306702/source/repos/test/load-impact-assessment/test-alert-monitor.js:77:16(59)
R

@woakesd, which k6 version are you using - as @imiric said, run k6 version ? If it’s not v0.26.0, can you update to it and try again?

Ah, I saw User-Agent: k6/0.25.1 (https://k6.io/) in your output, so yeah, please update the k6 version and try again. The issue you’re hitting seems like it might be fix(ws): wrongly sending nil as error when write on close fails by mstoykov · Pull Request #1118 · grafana/k6 · GitHub, which has been fixed in k6 v0.26.0.

I will do. I’ve worked out how to make it work. I didn’t quite understand the messages in chrome originally and thought I was only receiving.

Turns out you send a protocol request ‘{“protocol”:“json”,“version”:1})\x1e’ when you connect and then send the handshake ‘{“type”:6}\x1e’ periodically.

Works now, and I updated to 0.26.0.

Thanks.

@woakesd
Would it be possible for you to share your minimal fully working example? I’m currently fighting with SignalR and raw websockets to write a load test.

Thanks

group(`page_3 - test websocket`, function() {
	// Get the connection id to use in the web socket connect
	var res = http.post(`${urlBase}/${huburl}/negotiate`, null, {
			"responseType": "text"
		}
	);
	var connectionId = res.json()["connectionId"];

	var url = `${webSocketBase}/${huburl}?id=${connectionId}`;
	
	var response = ws.connect(url, {
			headers: {
				"Cookie": `${authCookieName}=${authCookie}`,
				"Origin": `https://${site}`,
			}
		}, function (socket) {
			socket.on('open', function open() {
				// Once socket is connected send protocol request
				socket.send('{"protocol":"json","version":1})\x1e');
				console.log('sent protocol request');
			  });
		
			socket.on('message', function (message) {
				switch (message) {
					case '{}\x1e':
						// This is the protocol confirmation
						break;
					case '{"type":6}\x1e':
						// Received handshake
						break;
					default:
						// should check that the JSON contains type === 1
						console.log(`Received message: ${message}`);
				}
			});
		
			socket.on('error', function (e) {
				if (e.error() != "websocket: close sent") {
					console.log('An unexpected error occurred: ', e.error());
				}
			});

			socket.setTimeout(function () {
				console.log('60 seconds passed, closing the socket');
				socket.close();
			}, 60000);
		});
	
		check(response, { "status is 101": (r) => r && r.status === 101 });

	}
);

The important bits are calling negotiate to get the id to pass to the websocket call and if you need an authorization cookie setting it’s value explicitly and once connected send the protocol message.

1 Like

Fantastic, thank you so much, I have a working connection now :slight_smile:

1 Like

@woakesd. Thanks for the solution it worked perfect!!. I was hesitant that this would not work on wss but after I tried this solution, it made my life easy.

1 Like