Debugging WebSockets: handshake, close codes, and reading a transcript
A WebSocket connection starts life as an ordinary HTTP GET with an `Upgrade: websocket` header. If the server answers `101 Switching Protocols`, the socket stays open and both sides can push frames whenever they like. Almost every failure happens in those first few milliseconds.
Testing without writing code
The WebSocket Tester opens the connection with the browser's native WebSocket API, so what you observe is exactly what your app would observe. Send text or JSON frames, and every message — outgoing, incoming and lifecycle event — lands in a timestamped transcript you can copy into a bug report.
Mixed content: the first thing to check
A page served over HTTPS may only open `wss://` sockets. Plain `ws://` is blocked before a single byte is sent. If nothing at all appears to happen, this is usually why.
Close codes worth memorising
- **1000** — normal closure; both sides agreed.
- **1001** — the endpoint is going away, typically a page navigation or server restart.
- **1006** — abnormal closure with no close frame. Almost always a network drop, a proxy timeout or a failed handshake; there is no protocol-level reason attached.
- **1008 / 1011** — policy violation and internal server error respectively; the server rejected you deliberately.
- **4000+** — application-defined. Your backend chose these, so check its source for the meaning.
When the handshake fails
Common causes: a wrong path, an origin allowlist on the server, an auth token expected in the query string (browsers cannot set custom headers on a WebSocket handshake), or a load balancer that does not forward the `Upgrade` header. That last one is the classic "works locally, dies in production" bug.
Keeping connections alive
Many proxies close idle sockets after 30–60 seconds. Send an application-level ping periodically, or configure the proxy's idle timeout. Reconnect logic should back off exponentially rather than hammering a server that is already unhealthy.