websockets/ws · warning · SyntaxError

Unexpected character at index ${i}

Error message

Unexpected character at index ${i}

What it means

Thrown by subprotocol.parse() while parsing the Sec-WebSocket-Protocol request header on the server. At lib/subprotocol.js:28-31, when a comma (0x2c) is encountered but start === -1 (no protocol token has begun), the parser throws SyntaxError indicating a comma appeared where a protocol name was expected. This is one of the malformed-header paths inside WebSocketServer.handleUpgrade() (websocket-server.js:286-292) where it is caught and converted into an HTTP 400 'Invalid Sec-WebSocket-Protocol header' response.

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 ae1de54330)

Solutions

  1. If consuming the error: listen for the 'wsClientError' event on WebSocketServer to log/inspect these clients, since by default they get a 400.
  2. On the client side, build the Sec-WebSocket-Protocol header by joining a non-empty list of valid token strings with a single comma.
  3. Sanitize/strip empty segments before setting the header if you proxy client requests.

Example fix

// before (client building header)
req.setHeader('Sec-WebSocket-Protocol', ['', 'chat'].join(','));

// after
req.setHeader('Sec-WebSocket-Protocol', ['chat'].join(','));
Defensive patterns

Strategy: try-catch

Validate before calling

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

Type guard

function looksLikeProtocolHeader(header) {
  return typeof header === 'string' && header.length > 0 && !header.startsWith(',');
}

Try / catch

wss.on('wsClientError', (err, socket, req) => {
  // default behavior (400) already applied; log/handle here
  console.warn('bad handshake from', req.socket.remoteAddress, err.message);
});

Prevention

When it happens

Trigger: A client sends a Sec-WebSocket-Protocol header that begins with a comma or contains consecutive commas, e.g. ',chat' or 'chat,,soap'. On the server side this is surfaced as a 400 handshake abort (or a 'wsClientError' event if a listener exists), not as a thrown error in user code. The raw throw is only seen if you call subprotocol.parse() directly.

Common situations: Client libraries that join an empty protocol list with commas; misconfigured proxies that prepend/append commas to header values; hand-crafted WebSocket clients with broken header construction.

Related errors


AI-assisted analysis of websockets/ws@ae1de54330 (2026-08-03). Data as JSON: /data/errors/d74676a1dc8d1822.json. Report an issue: GitHub.