websockets/ws · error · SyntaxError

An invalid or duplicated subprotocol was specified

Error message

An invalid or duplicated subprotocol was specified

What it means

Thrown by initAsClient() at websocket.js:781-788 when validating the protocols array passed to the WebSocket client constructor. Each entry must be (a) a string, (b) match the subprotocolRegex /^[!#$%&'*+-.0-9A-Z^_`|a-z~]+$/ (websocket.js:39), and (c) not already seen (duplicates rejected via protocolSet). Any failure throws a SyntaxError synchronously during construction.

Solutions

  1. Validate each protocol against the token regex and dedupe before connecting: protocols.filter(p => typeof p === 'string' && /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/.test(p)).
  2. Use simple alphanumeric/dot names for subprotocols (e.g. 'chat.v2', 'json') that always satisfy the regex.
  3. Dedupe: [...new Set(protocols)] to avoid duplicate-triggered failures.

Example fix

// before
const ws = new WebSocket(url, ['chat v2', 'chat v2']);

// after
const ws = new WebSocket(url, [...new Set(['chat.v2'])]);
Defensive patterns

Strategy: validation

Validate before calling

const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
function sanitizeClientProtocols(protocols) {
  if (!Array.isArray(protocols)) protocols = [protocols];
  const seen = new Set();
  const out = [];
  for (const p of protocols) {
    if (typeof p === 'string' && subprotocolRegex.test(p) && !seen.has(p)) {
      seen.add(p);
      out.push(p);
    }
  }
  return out;
}
// usage: new WebSocket(url, sanitizeClientProtocols(list));

Type guard

const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
function areValidClientProtocols(protocols) {
  if (!Array.isArray(protocols)) return false;
  const seen = new Set();
  return protocols.every(p =>
    typeof p === 'string' && subprotocolRegex.test(p) && !seen.has(p) && seen.add(p)
  );
}

Prevention

When it happens

Trigger: Passing protocols containing non-token characters (e.g. 'chat v2' with a space, 'a:b'); passing a non-string element (a number, object, or null in the array); passing duplicates like ['chat', 'chat']; passing an empty string ''. The loop at websocket.js:780-785 checks all three conditions per element.

Common situations: User-supplied protocol names that include spaces, colons, or slashes; a protocol list built from unvalidated input; passing a single non-string value where the constructor wraps it in an array (websocket.js:84) and then the loop rejects it; accidental duplicates from merging config sources.

Related errors


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

Appendix: source

Thrown at lib/websocket.js:786

  if (opts.perMessageDeflate) {
    perMessageDeflate = new PerMessageDeflate({
      ...opts.perMessageDeflate,
      isServer: false,
      maxPayload: opts.maxPayload
    });
    opts.headers['Sec-WebSocket-Extensions'] = format({
      [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
    });
  }
  if (protocols.length) {
    for (const protocol of protocols) {
      if (
        typeof protocol !== 'string' ||
        !subprotocolRegex.test(protocol) ||
        protocolSet.has(protocol)
      ) {
        throw new SyntaxError(
          'An invalid or duplicated subprotocol was specified'
        );
      }

      protocolSet.add(protocol);
    }

    opts.headers['Sec-WebSocket-Protocol'] = protocols.join(',');
  }
  if (opts.origin) {
    if (opts.protocolVersion < 13) {
      opts.headers['Sec-WebSocket-Origin'] = opts.origin;
    } else {
      opts.headers.Origin = opts.origin;
    }
  }
  if (parsedUrl.username || parsedUrl.password) {
    opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;

View on GitHub (pinned to c791e707ea)