6.1 KiB
Bug Fix: onSessionCreated Event Not Firing (v2.19.0)
Summary
Fixed critical bug where onSessionCreated lifecycle event was never emitted for sessions created during the standard MCP initialize flow, completely breaking session persistence functionality.
Impact
- Severity: Critical
- Affected Version: v2.19.0
- Component: Session Persistence (Phase 3)
- Status: ✅ Fixed
Root Cause
The handleRequest() method in http-server-single-session.ts had two different paths for session creation:
- Standard initialize flow (lines 868-943): Created session inline but did not emit
onSessionCreatedevent - Manual restoration flow (line 1048): Called
createSession()which correctly emitted the event
This inconsistency meant that:
- New sessions during normal operation were never saved to database
- Only manually restored sessions triggered the save event
- Session persistence was completely broken for new sessions
- Container restarts caused all sessions to be lost
The Fix
Location
- File:
src/http-server-single-session.ts - Method:
handleRequest() - Line: After line 943 (
await server.connect(transport);)
Code Change
Added event emission after successfully connecting server to transport during initialize flow:
// Connect the server to the transport BEFORE handling the request
logger.info('handleRequest: Connecting server to new transport');
await server.connect(transport);
// Phase 3: Emit onSessionCreated event (REQ-4)
// Fire-and-forget: don't await or block session creation
this.emitEvent('onSessionCreated', sessionIdToUse, instanceContext).catch(eventErr => {
logger.error('Failed to emit onSessionCreated event (non-blocking)', {
sessionId: sessionIdToUse,
error: eventErr instanceof Error ? eventErr.message : String(eventErr)
});
});
Why This Works
- Consistent with existing pattern: Matches the
createSession()method pattern (line 664) - Non-blocking: Uses
.catch()to ensure event handler errors don't break session creation - Correct timing: Fires after
server.connect(transport)succeeds, ensuring session is fully initialized - Same parameters: Passes
sessionIdandinstanceContextjust like the restoration flow
Verification
Test Results
Created comprehensive test suite to verify the fix:
Test File: tests/unit/session/onSessionCreated-event.test.ts
Test Results:
✓ onSessionCreated Event - Initialize Flow
✓ should emit onSessionCreated event when session is created during initialize flow (1594ms)
Test Files 5 passed (5)
Tests 78 passed (78)
Manual Testing:
const server = new SingleSessionHTTPServer({
sessionEvents: {
onSessionCreated: async (sessionId, context) => {
console.log('✅ Event fired:', sessionId);
await saveSessionToDatabase(sessionId, context);
}
}
});
// Result: Event fires successfully on initialize!
// ✅ Event fired: 40dcc123-46bd-4994-945e-f2dbe60e54c2
Behavior After Fix
- Initialize request → Session created →
onSessionCreatedevent fired → Session saved to database ✅ - Session restoration →
createSession()called →onSessionCreatedevent fired → Session saved to database ✅ - Manual restoration →
manuallyRestoreSession()→ Session created → Event fired ✅
All three paths now correctly emit the event!
Backward Compatibility
✅ Fully backward compatible:
- No breaking changes to API
- Event handler is optional (defaults to no-op)
- Non-blocking implementation ensures session creation succeeds even if handler fails
- Matches existing behavior of
createSession()method - All existing tests pass
Related Code
Event Emission Points
- ✅ Standard initialize flow:
handleRequest()at line ~947 (NEW - fixed) - ✅ Manual restoration:
createSession()at line 664 (EXISTING - working) - ✅ Session restoration: calls
createSession()indirectly (EXISTING - working)
Other Lifecycle Events
The following events are working correctly:
onSessionRestored: Fires when session is restored from databaseonSessionAccessed: Fires on every request (with throttling recommended)onSessionExpired: Fires before expired session cleanuponSessionDeleted: Fires on manual session deletion
Testing Recommendations
After applying this fix, verify session persistence works:
// 1. Start server with session events
const engine = new N8NMCPEngine({
sessionEvents: {
onSessionCreated: async (sessionId, context) => {
await database.upsertSession({ sessionId, ...context });
}
}
});
// 2. Client connects and initializes
// 3. Verify session saved to database
const sessions = await database.query('SELECT * FROM mcp_sessions');
expect(sessions.length).toBeGreaterThan(0);
// 4. Restart server
await engine.shutdown();
await engine.start();
// 5. Client reconnects with old session ID
// 6. Verify session restored from database
Impact on n8n-mcp-backend
This fix unblocks the multi-tenant n8n-mcp-backend service that depends on session persistence:
- ✅ Sessions now persist across container restarts
- ✅ Users no longer need to restart Claude Desktop after backend updates
- ✅ Session continuity maintained for all users
- ✅ Production deployment viable
Lessons Learned
- Consistency is critical: Session creation should follow the same pattern everywhere
- Event-driven architecture: Events must fire at all creation points, not just some
- Testing lifecycle events: Need integration tests that verify events fire, not just that code runs
- Documentation: Clearly document when events should fire and where
Files Changed
src/http-server-single-session.ts: Added event emission (lines 945-952)tests/unit/session/onSessionCreated-event.test.ts: New test filetests/integration/session/test-onSessionCreated-event.ts: Manual verification test
Build Status
- ✅ TypeScript compilation: Success
- ✅ Type checking: Success
- ✅ All unit tests: 78 passed
- ✅ Integration tests: Pass
- ✅ Backward compatibility: Verified