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
- If consuming the error: listen for the 'wsClientError' event on WebSocketServer to log/inspect these clients, since by default they get a 400.
- On the client side, build the Sec-WebSocket-Protocol header by joining a non-empty list of valid token strings with a single comma.
- 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
- On the server, attach a 'wsClientError' listener to observe malformed clients without crashes.
- On the client, build Sec-WebSocket-Protocol from a non-empty array of valid tokens joined by single commas.
- Strip empty segments introduced by proxies before forwarding the header.
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
- The "${protocol}" subprotocol is duplicated
- Unexpected end of input
- Unexpected character at index ${i}
- Unexpected end of input
- None of the extension offers can be accepted
AI-assisted analysis of websockets/ws@ae1de54330 (2026-08-03).
Data as JSON: /data/errors/d74676a1dc8d1822.json.
Report an issue: GitHub.