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:
Torsten Schulz (local)
2025-11-29 00:52:29 +01:00
parent bf0d5b0935
commit a651113dee
4 changed files with 270 additions and 20 deletions

View File

@@ -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);
}

View File

@@ -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;
};