Skip to content
Automation
Skill

/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

From plugin
relay
79017 skills39 agents
Install
$ npx -y skills add AgentWorkforce/relay --skill debugging-websocket-issues --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/debugging-websocket-issues

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

debugging-websocket-issues.SKILL.md
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

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 `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 |

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:**

1. Client connects to `/ws` 2. BOTH upgrade handlers fire (Node.js EventEmitter calls all listeners) 3. `wss1` matches path, handles upgrade successfully 4. `wss2` doesn't match, calls `abortHandshake(socket, 400)` 5. Raw `HTTP/1.1 400 Bad Request` written to the now-WebSocket socket 6. Client receives HTTP text as WebSocket frame data 7. First byte `0x48` ('H') interpreted as: RSV1=1, opcode=8 → invalid frame

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

  • `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)

Common Mistakes

| 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" |

Verification Checklist

After fixing, verify:

  • [ ] `RSV1: false` in frame inspection
  • [ ] `Extensions header: NONE` in upgrade response
  • [ ] No `HTTP/1.1` in raw frame data
  • [ ] Messages received match sent payload size
  • [ ] Multiple broadcasts work (test interval sends)
Read more
Ships withrelay

Let Claude Code message Codex. Let your Hyperagent talk to your Hermes agent. Give your custom agents a way to message each other.

Get the whole plugin