Marketplace 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
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/agentworkforce/debugging-websocket-issues" ~/.claude/skills/aiskillstore-marketplace-debugging-websocket-issues && rm -rf "$T"
manifest:
skills/agentworkforce/debugging-websocket-issues/SKILL.mdsource content
Debugging WebSocket Issues
Overview
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.
When to Use
- Error:
Invalid WebSocket frame: RSV1 must be clear - Error:
WS_ERR_UNEXPECTED_RSV_1 - Error:
Invalid frame header - WebSocket connects then immediately disconnects with code 1006
- Server logs success but client receives garbage data
Quick Reference
| Symptom | Likely Cause | Fix |
|---|---|---|
| RSV1 must be clear | Multiple WSS on same server OR compression mismatch | Use mode |
Hex starts with | Raw HTTP on WebSocket (0x48='H') | Check for conflicting upgrade handlers |
| Code 1006, no reason | Abnormal closure, often server-side abort | Check calls |
| Works isolated, fails in app | Something else writing to socket | Audit all upgrade listeners |
The Multiple WebSocketServer Bug
Problem
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:
- Client connects to
/ws - BOTH upgrade handlers fire (Node.js EventEmitter calls all listeners)
matches path, handles upgrade successfullywss1
doesn't match, callswss2abortHandshake(socket, 400)- Raw
written to the now-WebSocket socketHTTP/1.1 400 Bad Request - Client receives HTTP text as WebSocket frame data
- First byte
('H') interpreted as: RSV1=1, opcode=8 → invalid frame0x48
Solution
Use
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(); } });
Debugging Techniques
Raw Frame Inspection
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); }; });
Key Hex Patterns
= FIN + text frame (normal)81
= FIN + binary frame (normal)82
= FIN + close frame (normal)88
= "HTTP" - raw HTTP on WebSocket (bug!)48545450
or similar with bit 6 set = compressed frame (RSV1=1)c1
Common Mistakes
| Mistake | Result | Fix |
|---|---|---|
Multiple WSS with option | HTTP 400 written to socket | Use |
(default in older ws) | RSV1 set on frames | Explicitly set |
| Not checking upgrade headers | Miss compression negotiation | Log header |
| Assuming RSV1 error = compression | Could be raw HTTP | Check if bytes decode as ASCII "HTTP" |
Verification Checklist
After fixing, verify:
-
in frame inspectionRSV1: false -
in upgrade responseExtensions header: NONE - No
in raw frame dataHTTP/1.1 - Messages received match sent payload size
- Multiple broadcasts work (test interval sends)