一键导入
debugging-websocket-issues
// Use when seeing WebSocket errors like "Invalid frame header", "RSV1 must be clear", or "WS_ERR_UNEXPECTED_RSV_1" - covers multiple WebSocketServer conflicts, compression issues, and raw frame debugging techniques
// Use when seeing WebSocket errors like "Invalid frame header", "RSV1 must be clear", or "WS_ERR_UNEXPECTED_RSV_1" - covers multiple WebSocketServer conflicts, compression issues, and raw frame debugging techniques
Use when you need Codex to coordinate multiple agents through Relaycast for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.
Use when creating Agent Skills packages (SKILL.md format) for Codex CLI, GitHub Copilot, or Amp - provides the agentskills.io specification with frontmatter constraints, directory structure, and validation rules
Use when testing web applications with visual verification - automates Chrome browser interactions, element selection, and screenshot capture for confirming UI functionality
Use when creating or improving Claude Code agents. Expert guidance on agent file structure, frontmatter, persona definition, tool access, model selection, and validation against schema.
Use when creating or publishing Claude Code hooks - covers executable format, event types, JSON I/O, exit codes, security requirements, and PRPM package structure
Use when creating or fixing .claude/rules/ files - provides correct paths frontmatter (not globs), glob patterns, and avoids Cursor-specific fields like alwaysApply
| name | debugging-websocket-issues |
| description | Use when seeing WebSocket errors like "Invalid frame header", "RSV1 must be clear", or "WS_ERR_UNEXPECTED_RSV_1" - covers multiple WebSocketServer conflicts, compression issues, and raw frame debugging techniques |
| tags | websocket, debugging, ws, node |
WebSocket "invalid frame header" errors often stem from raw HTTP being written to an upgraded socket, not actual frame corruption. The most common cause is multiple WebSocketServer instances conflicting on the same HTTP server.
Invalid WebSocket frame: RSV1 must be clearWS_ERR_UNEXPECTED_RSV_1Invalid frame header| Symptom | Likely Cause | Fix |
|---|---|---|
| RSV1 must be clear | Multiple WSS on same server OR compression mismatch | Use noServer: true mode |
Hex starts with 48545450 | Raw HTTP on WebSocket (0x48='H') | Check for conflicting upgrade handlers |
| Code 1006, no reason | Abnormal closure, often server-side abort | Check abortHandshake calls |
| Works isolated, fails in app | Something else writing to socket | Audit all upgrade listeners |
When attaching multiple WebSocketServer instances to the same HTTP server using the server option:
// ❌ BAD - Both servers add upgrade listeners, causing conflicts
const wss1 = new WebSocketServer({ server, path: '/ws' });
const wss2 = new WebSocketServer({ server, path: '/ws/other' });
What happens:
/wswss1 matches path, handles upgrade successfullywss2 doesn't match, calls abortHandshake(socket, 400)HTTP/1.1 400 Bad Request written to the now-WebSocket socket0x48 ('H') interpreted as: RSV1=1, opcode=8 → invalid frameUse noServer: true and manually route upgrades:
// ✅ GOOD - Single upgrade handler routes to correct server
const wss1 = new WebSocketServer({ noServer: true, perMessageDeflate: false });
const wss2 = new WebSocketServer({ noServer: true, perMessageDeflate: false });
server.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url || '', `http://${request.headers.host}`).pathname;
if (pathname === '/ws') {
wss1.handleUpgrade(request, socket, head, (ws) => {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/ws/other') {
wss2.handleUpgrade(request, socket, head, (ws) => {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
Hook into the socket to see actual bytes received:
ws.on('open', () => {
const socket = ws._socket;
const originalPush = socket.push.bind(socket);
socket.push = function (chunk, encoding) {
if (chunk) {
console.log('First 20 bytes (hex):', chunk.slice(0, 20).toString('hex'));
const byte0 = chunk[0];
console.log(`FIN: ${!!(byte0 & 0x80)}, RSV1: ${!!(byte0 & 0x40)}, Opcode: ${byte0 & 0x0f}`);
// Check if it's actually HTTP text
if (chunk.slice(0, 4).toString() === 'HTTP') {
console.log('*** RECEIVED RAW HTTP ON WEBSOCKET ***');
}
}
return originalPush(chunk, encoding);
};
});
81 = FIN + text frame (normal)82 = FIN + binary frame (normal)88 = FIN + close frame (normal)48545450 = "HTTP" - raw HTTP on WebSocket (bug!)c1 or similar with bit 6 set = compressed frame (RSV1=1)| Mistake | Result | Fix |
|---|---|---|
Multiple WSS with server option | HTTP 400 written to socket | Use noServer: true |
perMessageDeflate: true (default in older ws) | RSV1 set on frames | Explicitly set perMessageDeflate: false |
| Not checking upgrade headers | Miss compression negotiation | Log sec-websocket-extensions header |
| Assuming RSV1 error = compression | Could be raw HTTP | Check if bytes decode as ASCII "HTTP" |
After fixing, verify:
RSV1: false in frame inspectionExtensions header: NONE in upgrade responseHTTP/1.1 in raw frame data