DevToys Web Pro iconDevToys Web Proब्लॉग
आम्हाला रेट करा:
ब्राउझर विस्तार वापरून पाहा:
← Back to Blog

WebSocket Tester Guide: Test ws:// and wss:// Connections in the Browser

8 min read

WebSocket connections fail quietly. A server that accepts HTTP requests just fine may reject the Upgrade handshake, drop idle connections after 60 seconds, or require a specific subprotocol header your client is not sending. The fastest way to isolate the problem is a browser-based client that lets you connect, send frames, and watch the raw message stream — no build step required. Use the WebSocket Tester to follow along with the examples in this guide.

ws:// vs wss://: Which Should You Use?

The WebSocket protocol has two URI schemes, mirroring the HTTP/HTTPS distinction:

SchemeTransportDefault portUse when
ws://Plain TCP80Local development, internal network (never over the public internet)
wss://TLS (same as HTTPS)443All production use; required when the page is served over HTTPS
http://TCP (no upgrade)80Not a WebSocket scheme — will fail with a protocol error

Browsers block ws:// connections from HTTPS pages as a mixed-content violation. If your page is on https://, you must use wss://. During local development on http://localhost, plain ws:// is permitted because localhost is treated as a secure context.

The TLS handshake in wss:// happens before the WebSocket Upgrade, so the entire HTTP Upgrade request — headers, subprotocol negotiation, and all frames — is encrypted. From a proxy or load balancer perspective, wss:// traffic looks identical tohttps:// traffic until the 101 response is sent.

The HTTP Upgrade Handshake

WebSocket connections start as a normal HTTP/1.1 request. The client sends an Upgrade header to ask the server to switch protocols. Here is what the exchange looks like:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

The Sec-WebSocket-Key is a random 16-byte value, base64-encoded. The server concatenates it with the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, computes a SHA-1 hash, and base64-encodes the result as Sec-WebSocket-Accept. This proves the server understood the WebSocket handshake and prevents accidental upgrades from non-WebSocket HTTP caches. A 101 Switching Protocols response means the connection is open; any other status code (400, 403, 426) means the upgrade was rejected — and the reason is usually in the response body.

The Browser WebSocket API

The browser exposes a WebSocket constructor that wraps the entire handshake. You provide the URL (and optionally a subprotocol list) and attach event handlers:

const ws = new WebSocket('wss://echo.websocket.org', ['chat']);

ws.onopen = (event) => {
  console.log('Connection opened, protocol:', ws.protocol);
  ws.send('Hello, server!');
};

ws.onmessage = (event) => {
  // event.data is a string for text frames,
  // Blob or ArrayBuffer for binary frames
  console.log('Received:', event.data);
};

ws.onclose = (event) => {
  // event.code is the WebSocket close code (1000 = normal, 1006 = abnormal)
  console.log('Closed:', event.code, event.reason);
};

ws.onerror = (event) => {
  // Error events carry no detail — check onclose for the code
  console.error('WebSocket error');
};

// Close cleanly when done
ws.close(1000, 'Work complete');

The readyState property tells you where the connection is in its lifecycle. It is useful for guarding sends — calling ws.send() when the socket is not yet open throws an InvalidStateError:

ValueConstantMeaning
0WebSocket.CONNECTINGHandshake in progress — do not send yet
1WebSocket.OPENConnection established — safe to send
2WebSocket.CLOSINGClose handshake initiated
3WebSocket.CLOSEDConnection closed or failed to open

Sending Text and Binary Frames

The WebSocket protocol has two data frame types: text (UTF-8) and binary. The browser send() method accepts a string (sent as a text frame), ArrayBuffer, TypedArray, or Blob (all sent as binary frames). Most JSON-based APIs use text frames exclusively.

// Text frame — most common for JSON APIs
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));

// Binary frame — useful for compact binary protocols
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 42, false); // big-endian uint32
ws.send(buffer);

// Receive binary as ArrayBuffer instead of Blob
ws.binaryType = 'arraybuffer'; // set before the first message

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    console.log('uint32:', view.getUint32(0, false));
  } else {
    console.log('text:', event.data);
  }
};

Ping/Pong and Idle Timeouts

WebSocket defines ping and pong control frames for keepalive. The server sends a ping; the client must respond with a pong. Browsers handle this automatically — you cannot send a ping from JavaScript (the browser WebSocket API does not expose it). What you can do is implement an application-level heartbeat using regular text frames:

let heartbeatInterval;

ws.onopen = () => {
  heartbeatInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    }
  }, 30_000); // every 30 seconds
};

ws.onclose = () => {
  clearInterval(heartbeatInterval);
};

Many infrastructure components — load balancers, proxies, NAT gateways — silently drop connections that have been idle for 60–90 seconds. If you see close code 1006 (abnormal closure, no close frame received) after a predictable idle period, a missing heartbeat is almost always the cause. The WebSocket Tester shows close codes and reasons in the message log, making it easy to confirm this pattern.

Subprotocols

The Sec-WebSocket-Protocol header lets the client advertise which application-level protocols it understands. The server picks one and echoes it back. If the server requires a subprotocol and the client does not send the header, the server will typically close the connection immediately after the handshake with a 4xxx close code.

// Request multiple subprotocols in priority order
const ws = new WebSocket('wss://example.com/ws', ['v2.chat', 'v1.chat']);

ws.onopen = () => {
  // ws.protocol is the one the server chose (empty string if none)
  console.log('Negotiated protocol:', ws.protocol);
};

Common subprotocol names include graphql-ws (GraphQL subscriptions), stomp (STOMP messaging), and mqtt. When debugging a connection that drops immediately after the handshake, check whether the server is expecting a specific subprotocol — it is a common gotcha that does not show up in the browser onerror handler.

Debugging Reconnection and Backoff Logic

Production WebSocket clients need reconnection logic. The standard pattern is exponential backoff with jitter to avoid thundering herds when a server restarts:

function connect(url, attempt = 0) {
  const ws = new WebSocket(url);

  ws.onopen = () => {
    console.log('Connected');
    attempt = 0; // reset backoff on success
  };

  ws.onclose = (event) => {
    if (event.code === 1000) return; // intentional close, do not reconnect

    // Exponential backoff: 1s, 2s, 4s, 8s … capped at 30s, with jitter
    const baseDelay = Math.min(1000 * 2 ** attempt, 30_000);
    const jitter = Math.random() * 0.3 * baseDelay;
    const delay = baseDelay + jitter;

    console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
    setTimeout(() => connect(url, attempt + 1), delay);
  };

  return ws;
}

When testing reconnection logic, manually close the connection from the WebSocket Tester with a specific close code to simulate different server behaviors: 1001 (server going away), 1011 (server error), or 1008 (policy violation). Each code should trigger a different path in your client logic.

Browser Client vs CLI Tools

CLI tools like wscat and websocat are excellent for server-side scripting and automation. A browser-based client has different strengths:

  • No installation — works immediately from any machine with a browser, including environments where you cannot install software.
  • Runs in the same origin context as your web app, so it respects the same CORS and mixed-content rules your users encounter.
  • The Network tab in DevTools records the Upgrade handshake and every frame when you open a WebSocket connection from a page — useful for seeing the raw headers alongside your application log.
  • Persistent message history during a session makes it easy to compare request/response pairs without scrolling a terminal.

For command-line use, install wscat with npm install -g wscat or websocat via your package manager:

# wscat
wscat -c wss://echo.websocket.org
> Hello
< Hello

# websocat
websocat wss://echo.websocket.org
Hello
Hello

# Connect with a custom header (wscat)
wscat -c wss://example.com/ws -H "Authorization: Bearer token123"

# Connect with a subprotocol (websocat)
websocat -p graphql-ws wss://example.com/graphql

When you need to share a reproducible test case with a colleague, a URL pointing to the WebSocket Tester with the endpoint pre-filled is faster than documenting CLI steps. If the WebSocket URL itself contains query parameters, use the URL Parser to verify the encoding before pasting it — a misencoded query parameter in the Upgrade request URL is a common source of 403 rejections. For more details on URL structure, see the URL Parser guide.


Open the WebSocket Tester to connect to any ws:// or wss:// endpoint, send text and binary frames, set custom headers, and watch the full message stream — all from the browser, with no installation needed.