websockets/ws · warning · SyntaxError

Unexpected character at index

Error message

Unexpected character at index ${i}

What it means

Thrown by subprotocol.parse() at subprotocol.js:29-30 when a comma (0x2c) is encountered but no protocol token has been started yet (start === -1). This means the Sec-WebSocket-Protocol header value contains a leading comma, a double comma, or a comma following only whitespace before any actual token characters. It is a server-side parse of the client's requested subprotocols.

Solutions

  1. If you control the client, ensure the subprotocol list has no empty entries before joining: protocols.filter(Boolean).join(',').
  2. On the server, the error is already caught inside handleUpgrade and results in a clean 400 handshake abort — no action needed unless you are calling subprotocol.parse() directly.
  3. If calling parse() directly, wrap it in try/catch and treat the failure as an invalid header (respond 400).

Example fix

// before (client)
ws = new WebSocket(url, ['', 'chat', '', 'update'].join(','));

// after (client)
ws = new WebSocket(url, ['', 'chat', '', 'update'].filter(Boolean).join(','));
Defensive patterns

Strategy: try-catch

Validate before calling

// Client-side: build the header safely
function buildProtocolHeader(protocols) {
  return protocols.filter(p => typeof p === 'string' && p.length > 0).join(',');
}
// usage: new WebSocket(url, buildProtocolHeader(list));

Type guard

function isValidProtocolHeader(header) {
  if (typeof header !== 'string' || header.length === 0) return false;
  try { require('ws/lib/subprotocol').parse(header); return true; }
  catch { return false; }
}

Try / catch

const { parse } = require('ws/lib/subprotocol');
let protocols;
try {
  protocols = parse(req.headers['sec-websocket-protocol']);
} catch (err) {
  // SyntaxError: malformed header -> reject handshake
  socket.destroy();
  return;
}

Prevention

When it happens

Trigger: A client sends a Sec-WebSocket-Protocol header like ',chat', 'chat,,update', or ' ,x'. On the server, handleUpgrade calls subprotocol.parse(secWebSocketProtocol) at websocket-server.js:287. Because start is still -1 when the comma is hit, the SyntaxError fires. (Note: handleUpgrade catches this and aborts the handshake with HTTP 400, so the error is internal unless you call parse() directly.)

Common situations: A buggy or hand-crafted client constructs the Sec-WebSocket-Protocol header by joining an array that contains empty strings (e.g. protocols.filter(...) producing gaps); a reverse proxy or load balancer appends to the header with a leading comma; a browser extension or non-compliant library sends a malformed header.

Related errors


AI-assisted analysis of websockets/ws@c791e707ea (2026-08-06). Data as JSON: /api/errors/d74676a1dc8d1822. Report an issue: GitHub.

Appendix: source

Thrown at lib/subprotocol.js:30

function parse(header) {
  const protocols = new Set();
  let start = -1;
  let end = -1;
  let i = 0;

  for (i; i < header.length; i++) {
    const code = header.charCodeAt(i);

    if (end === -1 && tokenChars[code] === 1) {
      if (start === -1) start = i;
    } else if (
      i !== 0 &&
      (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
    ) {
      if (end === -1 && start !== -1) end = i;
    } else if (code === 0x2c /* ',' */) {
      if (start === -1) {
        throw new SyntaxError(`Unexpected character at index ${i}`);
      }

      if (end === -1) end = i;

      const protocol = header.slice(start, end);

      if (protocols.has(protocol)) {
        throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
      }

      protocols.add(protocol);
      start = end = -1;
    } else {
      throw new SyntaxError(`Unexpected character at index ${i}`);
    }
  }

  if (start === -1 || end !== -1) {

View on GitHub (pinned to c791e707ea)