Enhance Socket.IO integration and improve error handling
- Updated CORS configuration for Socket.IO to allow all origins and added specific allowed headers. - Improved error handling for Socket.IO connections, including detailed logging for connection errors and upgrade attempts. - Implemented cleanup logic for socket connections during page reloads to prevent stale connections. - Enhanced reconnection logic with unlimited attempts and improved logging for connection status. - Updated frontend socket service to manage club room joining and leaving more effectively, with better state handling. - Configured Vite for improved hot module replacement (HMR) settings to support local development.
This commit is contained in:
@@ -60,12 +60,14 @@ const port = process.env.PORT || 3005;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// CORS-Konfiguration - Socket.IO hat seine eigene CORS-Konfiguration
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request Logging Middleware - loggt alle API-Requests
|
||||
@@ -117,6 +119,11 @@ app.use('/api/training-times', trainingTimeRoutes);
|
||||
|
||||
// Middleware für dynamischen kanonischen Tag (vor express.static)
|
||||
const setCanonicalTag = (req, res, next) => {
|
||||
// Socket.IO-Requests komplett ignorieren
|
||||
if (req.path.startsWith('/socket.io/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Nur für HTML-Anfragen (nicht für API, Assets, etc.)
|
||||
if (req.path.startsWith('/api') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|webmanifest|xml|txt)$/)) {
|
||||
return next();
|
||||
@@ -316,13 +323,13 @@ app.use((err, req, res, next) => {
|
||||
|
||||
// Erstelle HTTP-Server für API
|
||||
const httpServer = createServer(app);
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`🚀 HTTP-Server läuft auf Port ${port}`);
|
||||
});
|
||||
|
||||
|
||||
// WICHTIG: Socket.IO muss VOR dem Server-Start initialisiert werden
|
||||
// damit es Upgrade-Requests abfangen kann
|
||||
let socketIOInitialized = false;
|
||||
|
||||
// Erstelle HTTPS-Server für Socket.IO (direkt mit SSL)
|
||||
const httpsPort = process.env.HTTPS_PORT || 3051;
|
||||
let socketIOInitialized = false;
|
||||
|
||||
// Prüfe, ob SSL-Zertifikate vorhanden sind
|
||||
const sslKeyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||||
@@ -335,28 +342,51 @@ app.use((err, req, res, next) => {
|
||||
cert: fs.readFileSync(sslCertPath)
|
||||
};
|
||||
|
||||
// Erstelle HTTPS-Server mit Express-App
|
||||
const httpsServer = https.createServer(httpsOptions, app);
|
||||
|
||||
// Initialisiere Socket.IO auf HTTPS-Server
|
||||
// Initialisiere Socket.IO auf HTTPS-Server VOR dem Listen
|
||||
initializeSocketIO(httpsServer);
|
||||
socketIOInitialized = true;
|
||||
|
||||
httpsServer.listen(httpsPort, '0.0.0.0', () => {
|
||||
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
|
||||
console.log(` Socket.IO Endpoint: https://tt-tagebuch.de:${httpsPort}/socket.io/`);
|
||||
});
|
||||
|
||||
// Error-Handling für HTTPS-Server
|
||||
httpsServer.on('error', (err) => {
|
||||
console.error('❌ HTTPS-Server Error:', err.message);
|
||||
console.error(' Code:', err.code);
|
||||
});
|
||||
|
||||
httpsServer.on('clientError', (err, socket) => {
|
||||
if (socket && !socket.destroyed) {
|
||||
console.error('❌ HTTPS-Server Client Error:', err.message);
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('⚠️ HTTPS-Server konnte nicht gestartet werden:', err.message);
|
||||
console.error(' Stack:', err.stack);
|
||||
console.log(' → Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||||
console.log(` Erwartete Pfade: ${sslKeyPath}, ${sslCertPath}`);
|
||||
}
|
||||
|
||||
// Fallback: Socket.IO auf HTTP-Server (wenn noch nicht initialisiert)
|
||||
// WICHTIG: VOR dem httpServer.listen() initialisieren
|
||||
if (!socketIOInitialized) {
|
||||
initializeSocketIO(httpServer);
|
||||
console.log(' ✅ Socket.IO erfolgreich auf HTTP-Server initialisiert');
|
||||
}
|
||||
|
||||
// HTTP-Server starten NACH Socket.IO-Initialisierung
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`🚀 HTTP-Server läuft auf Port ${port}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Unable to synchronize the database:', err);
|
||||
}
|
||||
|
||||
@@ -3,46 +3,140 @@ import { Server } from 'socket.io';
|
||||
let io = null;
|
||||
|
||||
export const initializeSocketIO = (httpServer) => {
|
||||
// Verhindere doppelte Initialisierung
|
||||
if (io) {
|
||||
console.warn('⚠️ Socket.IO wurde bereits initialisiert. Überspringe erneute Initialisierung.');
|
||||
return io;
|
||||
}
|
||||
|
||||
io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: true,
|
||||
origin: (origin, callback) => {
|
||||
// Erlaube alle Origins für Socket.IO (da wir direkten HTTPS-Zugang haben)
|
||||
callback(null, true);
|
||||
},
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST']
|
||||
methods: ['GET', 'POST'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
|
||||
},
|
||||
// Wichtig für Reverse Proxy (Apache/Nginx): Path und Transports explizit setzen
|
||||
path: '/socket.io/',
|
||||
transports: ['websocket', 'polling'],
|
||||
transports: ['polling', 'websocket'], // Polling zuerst für bessere Kompatibilität
|
||||
// Erlaube Upgrade von polling zu websocket
|
||||
allowUpgrades: true,
|
||||
// Timeout für Upgrade
|
||||
upgradeTimeout: 10000,
|
||||
// Timeout für Upgrade - erhöht für bessere Kompatibilität
|
||||
upgradeTimeout: 30000,
|
||||
// Ping-Timeout für Verbindungen
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
// Wichtig für Reverse Proxy: Erlaube WebSocket-Upgrades auch ohne Origin-Header
|
||||
allowEIO3: true,
|
||||
// Erlaube WebSocket-Upgrades auch wenn der Request von einem Proxy kommt
|
||||
serveClient: false
|
||||
serveClient: false,
|
||||
// Verbesserte Cookie-Handling für Cross-Origin
|
||||
cookie: {
|
||||
name: 'io',
|
||||
httpOnly: false,
|
||||
sameSite: 'lax',
|
||||
path: '/'
|
||||
},
|
||||
// Erlaube Cross-Origin-Verbindungen
|
||||
connectTimeout: 45000,
|
||||
// Erlaube WebSocket-Upgrades auch bei Cross-Origin
|
||||
perMessageDeflate: false, // Deaktiviert für bessere Kompatibilität
|
||||
maxHttpBufferSize: 1e6 // 1MB
|
||||
});
|
||||
|
||||
// Verbesserte WebSocket-Upgrade-Logging
|
||||
io.engine.on('upgrade', (req, socket, head) => {
|
||||
console.log(`🔄 Socket.IO Upgrade-Versuch: ${req.url}`);
|
||||
console.log(` Headers:`, req.headers);
|
||||
});
|
||||
|
||||
io.engine.on('upgradeError', (err, req, socket, head) => {
|
||||
console.error('❌ Socket.IO Upgrade Error:', err.message || err);
|
||||
if (req) {
|
||||
console.error(' URL:', req.url);
|
||||
console.error(' Headers:', req.headers);
|
||||
console.error(' Method:', req.method);
|
||||
}
|
||||
if (err.stack) {
|
||||
console.error(' Stack:', err.stack);
|
||||
}
|
||||
});
|
||||
|
||||
// Error-Handling für Socket.IO
|
||||
io.engine.on('connection_error', (err) => {
|
||||
// "Session ID unknown" ist normal nach Server-Neustarts - weniger alarmierend loggen
|
||||
if (err.message && err.message.includes('Session ID unknown')) {
|
||||
console.warn('⚠️ Socket.IO: Unbekannte Session-ID (normal nach Server-Neustart)');
|
||||
if (err.context && err.context.sid) {
|
||||
console.warn(` Session-ID: ${err.context.sid}`);
|
||||
}
|
||||
// Client wird automatisch reconnecten - kein weiteres Action nötig
|
||||
return;
|
||||
}
|
||||
|
||||
// Andere Fehler normal loggen
|
||||
console.error('❌ Socket.IO Connection Error:', err.message || err);
|
||||
if (err.req) {
|
||||
console.error(' Request:', err.req.url);
|
||||
console.error(' Method:', err.req.method);
|
||||
}
|
||||
console.error(' Code:', err.code);
|
||||
console.error(' Type:', err.type);
|
||||
console.error(' Context:', err.context);
|
||||
if (err.stack) {
|
||||
console.error(' Stack:', err.stack);
|
||||
}
|
||||
});
|
||||
|
||||
// Detailliertes Request-Logging für Debugging
|
||||
io.engine.on('initial_headers', (headers, req) => {
|
||||
// Logge nur bei Problemen
|
||||
if (process.env.DEBUG_SOCKETIO === 'true') {
|
||||
console.log('📡 Socket.IO Request:', req.method, req.url);
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`✅ Socket.IO Client verbunden: ${socket.id}`);
|
||||
console.log(` Transport: ${socket.conn.transport.name}`);
|
||||
console.log(` Origin: ${socket.handshake.headers.origin || 'unknown'}`);
|
||||
|
||||
// Logge Transport-Upgrades
|
||||
socket.conn.on('upgrade', () => {
|
||||
console.log(`🔄 Socket.IO Transport Upgrade für ${socket.id}: ${socket.conn.transport.name}`);
|
||||
});
|
||||
|
||||
socket.conn.on('upgradeError', (error) => {
|
||||
console.error(`❌ Socket.IO Transport Upgrade Error für ${socket.id}:`, error.message);
|
||||
});
|
||||
|
||||
// Client tritt einem Club-Raum bei
|
||||
socket.on('join-club', (clubId) => {
|
||||
const room = `club-${clubId}`;
|
||||
socket.join(room);
|
||||
console.log(` Client ${socket.id} tritt Raum bei: ${room}`);
|
||||
});
|
||||
|
||||
// Client verlässt einen Club-Raum
|
||||
socket.on('leave-club', (clubId) => {
|
||||
const room = `club-${clubId}`;
|
||||
socket.leave(room);
|
||||
console.log(` Client ${socket.id} verlässt Raum: ${room}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// Socket getrennt
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`❌ Socket.IO Client getrennt: ${socket.id}, Grund: ${reason}`);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error(`❌ Socket.IO Client Error für ${socket.id}:`, error);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ Socket.IO erfolgreich initialisiert');
|
||||
return io;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,61 @@ import { io } from 'socket.io-client';
|
||||
import { backendBaseUrl } from '../apiClient.js';
|
||||
|
||||
let socket = null;
|
||||
let isReloading = false;
|
||||
let suppressLogs = false;
|
||||
|
||||
// Cleanup beim Seitenreload
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
isReloading = true;
|
||||
suppressLogs = true;
|
||||
if (socket) {
|
||||
// Trenne die Verbindung beim Reload, aber unterdrücke Logs
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Nach dem Reload Logs wieder erlauben (nach kurzer Verzögerung)
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
isReloading = false;
|
||||
suppressLogs = false;
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
export const connectSocket = (clubId) => {
|
||||
// Beim ersten Connect nach Reload, entferne alte Socket.IO Cookies
|
||||
if (typeof document !== 'undefined' && !socket) {
|
||||
// Entferne alte Socket.IO Session-Cookies
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const eqPos = cookie.indexOf('=');
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
||||
if (name === 'io') {
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wenn bereits eine Verbindung existiert, aber nicht verbunden ist, trenne sie
|
||||
if (socket && !socket.connected) {
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
if (socket && socket.connected) {
|
||||
// Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei
|
||||
if (socket.currentClubId) {
|
||||
if (socket.currentClubId && socket.currentClubId !== clubId) {
|
||||
socket.emit('leave-club', socket.currentClubId);
|
||||
socket.currentClubId = clubId;
|
||||
socket.emit('join-club', clubId);
|
||||
return socket;
|
||||
} else if (socket.currentClubId === clubId) {
|
||||
// Bereits im richtigen Club-Raum - keine Aktion nötig
|
||||
return socket;
|
||||
}
|
||||
} else {
|
||||
// Neue Verbindung erstellen
|
||||
@@ -33,27 +82,98 @@ export const connectSocket = (clubId) => {
|
||||
transports: ['polling', 'websocket'], // Polling zuerst für bessere Kompatibilität, dann WebSocket
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: Infinity, // Unbegrenzte Versuche
|
||||
timeout: 20000,
|
||||
upgrade: true,
|
||||
forceNew: false,
|
||||
secure: isHttps, // Nur für HTTPS
|
||||
rejectUnauthorized: false // Für selbst-signierte Zertifikate (nur Entwicklung)
|
||||
rejectUnauthorized: false, // Für selbst-signierte Zertifikate (nur Entwicklung)
|
||||
// Verbesserte Cookie-Handling
|
||||
withCredentials: true,
|
||||
// Auto-Connect
|
||||
autoConnect: true
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ Socket.IO verbunden:', socket.id);
|
||||
console.log(' Transport:', socket.io.engine.transport.name);
|
||||
// Wenn bereits ein Club ausgewählt war, trete dem Raum bei
|
||||
if (socket.currentClubId) {
|
||||
socket.emit('join-club', socket.currentClubId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// Socket getrennt
|
||||
socket.on('disconnect', (reason) => {
|
||||
// Beim Reload oder wenn Logs unterdrückt werden sollen, keine Logs
|
||||
if (isReloading || suppressLogs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "transport error" ist normal beim Reload - unterdrücke diese Logs komplett
|
||||
if (reason === 'transport error' || reason === 'ping timeout') {
|
||||
return; // Keine Logs für normale Transport-Fehler
|
||||
}
|
||||
|
||||
// Nur wichtige Disconnects loggen
|
||||
if (reason !== 'io client disconnect' && reason !== 'transport close') {
|
||||
console.log('❌ Socket.IO getrennt:', reason);
|
||||
}
|
||||
|
||||
if (reason === 'io server disconnect') {
|
||||
// Server hat die Verbindung getrennt, neu verbinden
|
||||
socket.connect();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Socket.IO Verbindungsfehler:', error);
|
||||
// Beim Reload keine Error-Logs
|
||||
if (isReloading || suppressLogs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "xhr poll error" oder "Session ID unknown" sind normal - unterdrücke diese
|
||||
if (error.message && (
|
||||
error.message.includes('Session ID unknown') ||
|
||||
error.message.includes('xhr poll error') ||
|
||||
error.type === 'TransportError'
|
||||
)) {
|
||||
// Keine Logs für normale Verbindungsfehler
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur unerwartete Fehler loggen
|
||||
console.error('❌ Socket.IO Verbindungsfehler:', error.message);
|
||||
console.error(' Type:', error.type);
|
||||
console.error(' Description:', error.description);
|
||||
});
|
||||
|
||||
socket.on('reconnect', (attemptNumber) => {
|
||||
// Beim Reload keine Reconnect-Logs
|
||||
if (!isReloading && !suppressLogs) {
|
||||
console.log('✅ Socket.IO wieder verbunden nach', attemptNumber, 'Versuch(en)');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_attempt', (attemptNumber) => {
|
||||
// Reconnect-Versuche nicht loggen - zu viele Logs
|
||||
// if (!isReloading && !suppressLogs) {
|
||||
// console.log('🔄 Socket.IO Verbindungsversuch', attemptNumber);
|
||||
// }
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', (error) => {
|
||||
// Beim Reload keine Error-Logs
|
||||
if (!isReloading && !suppressLogs) {
|
||||
console.error('❌ Socket.IO Wiederverbindungsfehler:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_failed', () => {
|
||||
// Nur kritische Fehler loggen
|
||||
if (!isReloading && !suppressLogs) {
|
||||
console.error('❌ Socket.IO Wiederverbindung fehlgeschlagen - alle Versuche aufgebraucht');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,6 +191,7 @@ export const disconnectSocket = () => {
|
||||
if (socket.currentClubId) {
|
||||
socket.emit('leave-club', socket.currentClubId);
|
||||
}
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ export default defineConfig({
|
||||
port: 5000,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
hmr: {
|
||||
protocol: 'ws',
|
||||
host: 'localhost',
|
||||
port: 5000,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user