Sending and receiving messages
Text, binary, JSON, size limits, and commands hidden in ordinary messages.
Once the handshake is done, the connection carries messages. The protocol knows very little about them: each one is either text or binary, and has a length. What goes inside is up to the application. That freedom is where most of the testing work lies, because every application invents its own rules.
Text and binary
A text message must be valid UTF-8. A binary message can be any bytes. In JavaScript you send a string for text, and an ArrayBuffer, typed array or Blob for binary. Here are four messages sent to the echo socket, with what came back:
ws.binaryType = "arraybuffer";
ws.send("hello"); // text
ws.send(JSON.stringify({ hello: "world" })); // text that happens to be JSON
ws.send(""); // an empty text message
ws.send(new Uint8Array([1, 2, 3, 255])); // binaryreceived: hello
received: {"hello":"world"}
received:
received: <binary 4 bytes: 010203ff>Each came back as the same kind it was sent as. That matters more than it looks. A server that echoes binary as text, or the reverse, breaks any client that checks the type. Note the empty message too: it's allowed, and it came back. A client that treats an empty string as "nothing received" will misbehave on a server that sends them.
In browsers, binary messages arrive as Blob objects unless you set binaryType = "arraybuffer". In Node they arrive as Blobs by default as well. Forgetting this is a common cause of "the message is [object Blob]" bugs.
Unicode survives too. Send héllo 👋 Привет and the same string comes back, byte for byte.
Messages appear here.
JSON messages
Most applications send JSON in text messages, usually with a field that says what kind of message it is. Both sockets on this API do: the echo socket's greeting has "type": "welcome", and every ticker message has "type": "tick" or "type": "complete". A client then routes each message by its type:
ws.addEventListener("message", (event) => {
let msg;
try {
msg = JSON.parse(event.data);
} catch {
console.warn("not JSON:", event.data);
return;
}
if (msg.type === "welcome") showBanner(msg.message);
else if (msg.type === "tick") updatePrice(msg.data);
else console.warn("unknown message type:", msg.type);
});The two warnings in that handler are where tests should aim. What does the client do with a message that isn't JSON? With a type it's never heard of? Servers add new message types over time, and a client that crashes on an unknown type will break the day the server ships one. The echo socket is handy for this kind of test, because you can make it send anything: send it the exact message you want your client to receive.
Replies aren't paired with requests
In HTTP, every response belongs to exactly one request. A WebSocket has no such link. If you send two questions and get two answers, the protocol doesn't say which answer goes with which question, and on a busy socket other messages can arrive in between. Applications that need request and reply add an ID themselves:
// Client
ws.send(JSON.stringify({ id: "req-17", action: "getPrice", symbol: "AAPL" }));
// Server, some time later, possibly after other messages
{ "id": "req-17", "result": { "symbol": "AAPL", "price": 230.19 } }If the API you test works this way, check that every reply carries the ID it was asked with, that replies to two quick requests aren't swapped, and what happens when you send a duplicate ID or none at all.
Order
Within one connection, messages arrive in the order they were sent. TCP guarantees that. Five hundred numbered messages fired at the echo socket as fast as possible came back as 0 to 499, in order. What isn't guaranteed is order across connections, or across a reconnect. A message sent just before a connection drops may never arrive at all, which the reconnecting lesson deals with.
Size limits
Servers limit how big a message can be, to stop one client using up all their memory. The echo socket's limit is 64KB, and going over it doesn't produce an error message. The server closes the connection:
ws.send("a".repeat(65536)); // echoed
ws.send("a".repeat(65537)); // close 1009 "Message exceeds 64KB"Close code 1009 means "message too big". Test both sides of the limit: exactly at it should work, one byte over should not. And notice the word byte. The limit counts bytes on the wire, but JavaScript's .lengthcounts UTF-16 code units, which aren't the same thing:
const msg = "👋".repeat(16384);
msg.length; // 32768
new TextEncoder().encode(msg).length; // 65536 bytes: echoed
const longer = msg + "a";
longer.length; // 32769
new TextEncoder().encode(longer).length; // 65537 bytes: close 1009Each emoji is 4 bytes in UTF-8 but only 2 units of .length. A client that checks msg.length <= 65536 before sending will happily send a 131,072-byte message of emoji and get disconnected. If the app enforces a limit, test it with multi-byte text, not just with "a".repeat(n).
Commands hidden in messages
The echo socket has one special message: sending close makes the server hang up instead of echoing. Special messages like this are common. Chat apps treat /leave as a command, and trading feeds take subscribe. They're also a reliable source of bugs, because the line between a command and ordinary data is fuzzier than it looks. Here's what the echo socket did with variations on the word:
| Message sent | What happened |
|---|---|
close | Disconnected, 1000 "Bye!" |
CLOSE | Disconnected |
Close | Disconnected |
close (leading space) | Disconnected |
close\n (trailing newline) | Disconnected |
"close" | Echoed (the JSON string, with its quotes) |
closed | Echoed |
please close | Echoed |
The server ignores case and surrounding whitespace. The docs say to send "the text close", which doesn't mention either. Is that a bug? It depends on who sends what. On an echo socket, nobody cares. In a chat app with the same logic, a user who types "Close" as a whole message gets disconnected. Whenever you find a special message, try it in other cases, with spaces, inside JSON, and as part of a longer message, then decide with the team which of those should count.
Exercise
Using the panel above, find out what the echo socket does with: a message of a single space; the JSON {"type":"welcome"}; and a message containing a newline in the middle. Then write down the size limit for the echo socket in a way that would be impossible to misread, given what you now know about bytes and .length.