TestingAPIs is still being built. If something breaks, email support@testingapis.com.

The opening handshake

How an HTTP request becomes a socket, and the ways it fails before the first message.

Every WebSocket starts life as an ordinary HTTP request. The client asks the server to stop speaking HTTP on this connection and switch to the WebSocket protocol. If the server agrees, the same TCP connection carries WebSocket messages from then on. This exchange is the handshake, and it's the one part of a WebSocket you can test with the HTTP tools you already know.

The request

When a browser opens wss://api.testingapis.com/ws/echosign in to see, it sends this:

Request
GET /ws/echo HTTP/1.1
Host: api.testingapis.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://testingapis.com
This example uses endpoints for signed-in members.Sign inCreate a free account

It's a GET, always. Upgrade: websocket and Connection: Upgrade together ask for the switch. Sec-WebSocket-Version is 13, the only version in use. Sec-WebSocket-Key is 16 random bytes in Base64, new for every connection. The browser adds Originon its own, naming the site whose page opened the socket. Script on the page can't change it, which makes it useful to servers for security, as lesson 8 explains.

The response

curl doesn't speak WebSocket, but it can send the handshake and print what comes back. The key here is the example from the RFC, so you can check the answer against the specification:

Terminal
$ curl -i -N --http1.1 https://api.testingapis.com/ws/echo \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=="

HTTP/1.1 101 Switching Protocols
connection: Upgrade
upgrade: websocket
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

��{"type":"welcome","message":"Connected to TestingAPIs echo. ..."}
This example uses endpoints for signed-in members.Sign inCreate a free account

101 Switching Protocols is the only status that means success. Anything else, including a 200, means no WebSocket. After the blank line the connection is no longer HTTP, and curl prints the first WebSocket frame as raw bytes. The two garbled characters at the start are the frame header. In hex:

Bytes
81 7e 00 8d 7b 22 74 79 70 65 22 3a 22 77 65 6c   .~..{"type":"wel
63 6f 6d 65 22 2c 22 6d 65 73 73 61 67 65 22 3a   come","message":

81 says this is a complete text message. 7e says the length is in the next two bytes, and 00 8dis 141: the welcome message is 141 bytes long. Then the JSON itself. You rarely need to read frames by hand, but it's good to know there's nothing mysterious in there.

Sec-WebSocket-Accept

The server proves it understood the request by sending back a value derived from the key: it appends a fixed string from the RFC, takes the SHA-1 hash, and encodes that in Base64. You can do the same:

Terminal
$ node -e "
const { createHash } = require('node:crypto');
const key = 'dGhlIHNhbXBsZSBub25jZQ==';
const guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
console.log(createHash('sha1').update(key + guid).digest('base64'));
"
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

It matches. The point isn't security. It stops a cache or a confused proxy from replaying an old response and convincing the client that a WebSocket opened. Client libraries check the value and give up if it's wrong, so a server that computes it incorrectly will fail with every client, and quickly.

When the handshake fails

Each of these requests to this API fails before a WebSocket exists. The responses come from curl:

RequestResponse
Plain GET, no upgrade headers400 with JSON: This endpoint only accepts WebSocket connections.
Sec-WebSocket-Version: 8400 with the same JSON message
No Sec-WebSocket-Key400 with the same JSON message
A path that doesn't exist, /ws/nopesign in to see404
/ws/ticker?count=abcsign in to see400 with The value 'abc' is not valid. for count
ws:// instead of wss://301 redirect to https://

Two of these need a closer look. For the wrong version, RFC 6455 asks the server to say which versions it does support, in a Sec-WebSocket-Versionheader, usually with status 426. This API sends neither, and its message claims the request wasn't a WebSocket request at all, when it was. Small, but a developer chasing a version mismatch would be misled.

The redirect is the other. WebSocket clients don't follow redirects; browsers treat any status other than 101 as failure. So a ws:// URL on this API never connects, even though the 400 message above suggests one. If a server moves a socket to a new address, old clients break instead of following it. Test the old URL after any move.

What the browser sees

Here is everything a browser or Node script learns when any of those handshakes fails:

Events
error
close 1006 ""  wasClean=false

An errorevent with no details, then a close with code 1006 and no reason. No status code, no headers, no JSON body. Browsers hide them on purpose, so a web page can't use WebSockets to probe servers it shouldn't reach. For testing, that has two consequences. Test the handshake with curl or another raw HTTP client, where you can see why it failed. And check what the app tells its users when the handshake fails, because the app doesn't know why either. "Couldn't connect, retrying" is honest. "Invalid ticker count" is impossible to show, because the app never received that message.

Subprotocols

A client can offer one or more named subprotocols, saying which message format it intends to speak, for example graphql-transport-ws for GraphQL subscriptions. The server picks one and names it in its response. If the client offered any and the server picks none, the client must fail the connection.

JavaScript
const ws = new WebSocket("wss://api.testingapis.com/ws/echo", ["chat.v1"]);
This example uses endpoints for signed-in members.Sign inCreate a free account

The echo socket doesn't use subprotocols. It accepts the handshake with a 101 but leaves Sec-WebSocket-Protocol out of its response, so the client gives up: an errorevent and close code 1006, even though the server was happy. If the API you test uses subprotocols, try one it doesn't support, none at all, and several in different orders, and check which one the server picks.

Exercise

Generate your own key: 16 random bytes, Base64-encoded (the Base64 tool or openssl rand -base64 16 will do). Send the handshake with curl using that key, then work out the expected Sec-WebSocket-Acceptwith the Node command above. Does the server's answer match? Now send a key that isn't valid Base64, such as hello. What happens, and what should happen?

Practise and go further