Fix: WebSocket-Konflikt zwischen Streaming und Watch Together

Root Cause: ws-Library killt WS-Verbindungen mit HTTP 400 wenn
mehrere WebSocketServer mit { server, path } registriert werden.
Der erste WSS (streaming) hat abortHandshake() fuer watch-together
Verbindungen aufgerufen.

- Beide WSS auf noServer-Modus umgestellt
- Zentrales upgrade-Routing in index.ts nach Pathname
- Frontend: connectWs prueft jetzt auch CONNECTING-State
- Frontend: onclose clobbert wsRef nur noch wenn gleicher Socket
- Frontend: waitForWs hat jetzt 10s Timeout statt Endlosschleife

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-07 11:02:36 +01:00
parent e748fc97e9
commit e4895a792c
6 changed files with 60 additions and 32 deletions

View file

@ -1,6 +1,7 @@
import express from 'express'; import express from 'express';
import path from 'node:path'; import path from 'node:path';
import http from 'node:http'; import http from 'node:http';
import { WebSocketServer } from 'ws';
import { Client } from 'discord.js'; import { Client } from 'discord.js';
import { createClient } from './core/discord.js'; import { createClient } from './core/discord.js';
import { addSSEClient, removeSSEClient, sseBroadcast, getSSEClientCount } from './core/sse.js'; import { addSSEClient, removeSSEClient, sseBroadcast, getSSEClientCount } from './core/sse.js';
@ -168,8 +169,29 @@ async function boot(): Promise<void> {
// Start Express (http.createServer so WebSocket can attach) // Start Express (http.createServer so WebSocket can attach)
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
attachWebSocket(httpServer);
attachWatchTogetherWs(httpServer); // Create WebSocket servers (noServer mode to avoid path conflicts)
const wssStreaming = new WebSocketServer({ noServer: true });
const wssWatchTogether = new WebSocketServer({ noServer: true });
// Route WebSocket upgrade requests to the correct server
httpServer.on('upgrade', (request, socket, head) => {
const { pathname } = new URL(request.url!, `http://${request.headers.host}`);
if (pathname === '/ws/streaming') {
wssStreaming.handleUpgrade(request, socket, head, (ws) => {
wssStreaming.emit('connection', ws, request);
});
} else if (pathname === '/ws/watch-together') {
wssWatchTogether.handleUpgrade(request, socket, head, (ws) => {
wssWatchTogether.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
attachWebSocket(wssStreaming);
attachWatchTogetherWs(wssWatchTogether);
httpServer.listen(PORT, () => console.log(`[HTTP] Listening on :${PORT}`)); httpServer.listen(PORT, () => console.log(`[HTTP] Listening on :${PORT}`));
// Login Discord bots // Login Discord bots

View file

@ -1,5 +1,4 @@
import type express from 'express'; import type express from 'express';
import http from 'node:http';
import { WebSocketServer, WebSocket } from 'ws'; import { WebSocketServer, WebSocket } from 'ws';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import type { Plugin, PluginContext } from '../../core/plugin.js'; import type { Plugin, PluginContext } from '../../core/plugin.js';
@ -265,9 +264,9 @@ const streamingPlugin: Plugin = {
}, },
}; };
/** Call after httpServer is created to attach WebSocket signaling */ /** Attach WebSocket signaling to a pre-created WebSocketServer (noServer mode) */
export function attachWebSocket(server: http.Server): void { export function attachWebSocket(existingWss: WebSocketServer): void {
wss = new WebSocketServer({ server, path: '/ws/streaming' }); wss = existingWss;
wss.on('connection', (ws) => { wss.on('connection', (ws) => {
const clientId = crypto.randomUUID(); const clientId = crypto.randomUUID();

View file

@ -1,5 +1,4 @@
import type express from 'express'; import type express from 'express';
import http from 'node:http';
import { WebSocketServer, WebSocket } from 'ws'; import { WebSocketServer, WebSocket } from 'ws';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import type { Plugin, PluginContext } from '../../core/plugin.js'; import type { Plugin, PluginContext } from '../../core/plugin.js';
@ -515,9 +514,9 @@ const watchTogetherPlugin: Plugin = {
}, },
}; };
/** Call after httpServer is created to attach WebSocket */ /** Attach WebSocket to a pre-created WebSocketServer (noServer mode) */
export function attachWatchTogetherWs(server: http.Server): void { export function attachWatchTogetherWs(existingWss: WebSocketServer): void {
wss = new WebSocketServer({ server, path: '/ws/watch-together' }); wss = existingWss;
wss.on('connection', (ws) => { wss.on('connection', (ws) => {
const clientId = crypto.randomUUID(); const clientId = crypto.randomUUID();

File diff suppressed because one or more lines are too long

2
web/dist/index.html vendored
View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gaming Hub</title> <title>Gaming Hub</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎮</text></svg>" /> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎮</text></svg>" />
<script type="module" crossorigin src="/assets/index-CgrjD6IO.js"></script> <script type="module" crossorigin src="/assets/index-DKV7w-rf.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C2eno-Si.css"> <link rel="stylesheet" crossorigin href="/assets/index-C2eno-Si.css">
</head> </head>
<body> <body>

View file

@ -367,7 +367,7 @@ export default function WatchTogetherTab({ data }: { data: any }) {
// ── WebSocket connect ── // ── WebSocket connect ──
const connectWs = useCallback(() => { const connectWs = useCallback(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return; if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) return;
const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${location.host}/ws/watch-together`); const ws = new WebSocket(`${proto}://${location.host}/ws/watch-together`);
@ -382,7 +382,9 @@ export default function WatchTogetherTab({ data }: { data: any }) {
}; };
ws.onclose = () => { ws.onclose = () => {
if (wsRef.current === ws) {
wsRef.current = null; wsRef.current = null;
}
if (currentRoomRef.current) { if (currentRoomRef.current) {
reconnectTimerRef.current = setTimeout(() => { reconnectTimerRef.current = setTimeout(() => {
reconnectDelayRef.current = Math.min(reconnectDelayRef.current * 2, 10000); reconnectDelayRef.current = Math.min(reconnectDelayRef.current * 2, 10000);
@ -401,6 +403,7 @@ export default function WatchTogetherTab({ data }: { data: any }) {
setError(null); setError(null);
connectWs(); connectWs();
const startTime = Date.now();
const waitForWs = () => { const waitForWs = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) { if (wsRef.current?.readyState === WebSocket.OPEN) {
wsSend({ wsSend({
@ -409,6 +412,8 @@ export default function WatchTogetherTab({ data }: { data: any }) {
userName: userName.trim(), userName: userName.trim(),
password: roomPassword.trim() || undefined, password: roomPassword.trim() || undefined,
}); });
} else if (Date.now() - startTime > 10000) {
setError('Verbindung zum Server fehlgeschlagen.');
} else { } else {
setTimeout(waitForWs, 100); setTimeout(waitForWs, 100);
} }
@ -422,6 +427,7 @@ export default function WatchTogetherTab({ data }: { data: any }) {
setError(null); setError(null);
connectWs(); connectWs();
const startTime = Date.now();
const waitForWs = () => { const waitForWs = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) { if (wsRef.current?.readyState === WebSocket.OPEN) {
wsSend({ wsSend({
@ -430,6 +436,8 @@ export default function WatchTogetherTab({ data }: { data: any }) {
roomId, roomId,
password: password?.trim() || undefined, password: password?.trim() || undefined,
}); });
} else if (Date.now() - startTime > 10000) {
setError('Verbindung zum Server fehlgeschlagen.');
} else { } else {
setTimeout(waitForWs, 100); setTimeout(waitForWs, 100);
} }