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
- If you control the client, ensure the subprotocol list has no empty entries before joining: protocols.filter(Boolean).join(',').
- 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.
- 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
- Filter empty strings from protocol arrays before joining into the header.
- On the server, rely on handleUpgrade which already catches parse errors and aborts with 400.
- If calling subprotocol.parse() directly, always wrap in try/catch.
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
- The " " subprotocol is duplicated
- Unexpected end of input
- An invalid or duplicated subprotocol was specified
- First argument must be a valid error code number
- The data size must not be greater than 125 bytes
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)