{"record":{"id":"d74676a1dc8d1822","repo":"websockets/ws","slug":"unexpected-character-at-index-i-d74676","errorCode":null,"errorMessage":"Unexpected character at index ${i}","messagePattern":"Unexpected character at index (.+?)","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"warning","filePath":"lib/subprotocol.js","lineNumber":30,"sourceCode":"function parse(header) {\n  const protocols = new Set();\n  let start = -1;\n  let end = -1;\n  let i = 0;\n\n  for (i; i < header.length; i++) {\n    const code = header.charCodeAt(i);\n\n    if (end === -1 && tokenChars[code] === 1) {\n      if (start === -1) start = i;\n    } else if (\n      i !== 0 &&\n      (code === 0x20 /* ' ' */ || code === 0x09) /* '\\t' */\n    ) {\n      if (end === -1 && start !== -1) end = i;\n    } else if (code === 0x2c /* ',' */) {\n      if (start === -1) {\n        throw new SyntaxError(`Unexpected character at index ${i}`);\n      }\n\n      if (end === -1) end = i;\n\n      const protocol = header.slice(start, end);\n\n      if (protocols.has(protocol)) {\n        throw new SyntaxError(`The \"${protocol}\" subprotocol is duplicated`);\n      }\n\n      protocols.add(protocol);\n      start = end = -1;\n    } else {\n      throw new SyntaxError(`Unexpected character at index ${i}`);\n    }\n  }\n\n  if (start === -1 || end !== -1) {","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/websockets/ws/blob/c791e707eab3c13dd9a261d2479c3cc4a49a6fed/lib/subprotocol.js#L12-L48","documentation":"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.","triggerScenarios":"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.)","commonSituations":"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.","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)."],"exampleFix":"// before (client)\nws = new WebSocket(url, ['', 'chat', '', 'update'].join(','));\n\n// after (client)\nws = new WebSocket(url, ['', 'chat', '', 'update'].filter(Boolean).join(','));","handlingStrategy":"try-catch","validationCode":"// Client-side: build the header safely\nfunction buildProtocolHeader(protocols) {\n  return protocols.filter(p => typeof p === 'string' && p.length > 0).join(',');\n}\n// usage: new WebSocket(url, buildProtocolHeader(list));","typeGuard":"function isValidProtocolHeader(header) {\n  if (typeof header !== 'string' || header.length === 0) return false;\n  try { require('ws/lib/subprotocol').parse(header); return true; }\n  catch { return false; }\n}","tryCatchPattern":"const { parse } = require('ws/lib/subprotocol');\nlet protocols;\ntry {\n  protocols = parse(req.headers['sec-websocket-protocol']);\n} catch (err) {\n  // SyntaxError: malformed header -> reject handshake\n  socket.destroy();\n  return;\n}","preventionTips":["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."],"tags":["websocket","subprotocol","header-parsing","rfc6455","server"],"backgroundTag":null,"analyzedSha":"c791e707eab3c13dd9a261d2479c3cc4a49a6fed","analyzedAt":"2026-08-06T19:07:51.047Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}