fix: eliminate stack overflow in session removal (#427) (#428)

Critical bug fix for production crashes during session cleanup.

**Root Cause:**
Infinite recursion caused by circular event handler chain:
- removeSession() called transport.close()
- transport.close() triggered onclose event handler
- onclose handler called removeSession() again
- Loop continued until stack overflow

**Solution:**
Delete transport from registry BEFORE closing to break circular reference:
1. Store transport reference
2. Delete from this.transports first
3. Close transport after deletion
4. When onclose fires, transport no longer found, no recursion

**Impact:**
- Eliminates "RangeError: Maximum call stack size exceeded" errors
- Fixes session cleanup crashes every 5 minutes in production
- Prevents potential memory leaks from failed cleanup

**Testing:**
- Added regression test for infinite recursion prevention
- All 39 session management tests pass
- Build and typecheck succeed

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Closes #427
This commit is contained in:
Romuald Członkowski
2025-11-18 17:41:17 +01:00
committed by GitHub
parent 1bbfaabbc2
commit 5575630711
4 changed files with 83 additions and 15 deletions

View File

@@ -155,17 +155,22 @@ export class SingleSessionHTTPServer {
*/
private async removeSession(sessionId: string, reason: string): Promise<void> {
try {
// Close transport if exists
if (this.transports[sessionId]) {
await this.transports[sessionId].close();
delete this.transports[sessionId];
}
// Remove server, metadata, and context
// Store reference to transport before deletion
const transport = this.transports[sessionId];
// Delete transport FIRST to prevent onclose handler from triggering recursion
// This breaks the circular reference: removeSession -> close -> onclose -> removeSession
delete this.transports[sessionId];
delete this.servers[sessionId];
delete this.sessionMetadata[sessionId];
delete this.sessionContexts[sessionId];
// Close transport AFTER deletion
// When onclose handler fires, it won't find the transport anymore
if (transport) {
await transport.close();
}
logger.info('Session removed', { sessionId, reason });
} catch (error) {
logger.warn('Error removing session', { sessionId, reason, error });