Compare commits

...

2 Commits

Author SHA1 Message Date
Romuald Członkowski
47d9f55dc5 chore: update n8n to 1.120.3 and bump version to 2.22.20 (#430)
- Updated n8n from 1.119.1 to 1.120.3
- Updated n8n-core from 1.118.0 to 1.119.2
- Updated n8n-workflow from 1.116.0 to 1.117.0
- Updated @n8n/n8n-nodes-langchain from 1.118.0 to 1.119.1
- Rebuilt node database with 544 nodes (439 from n8n-nodes-base, 105 from @n8n/n8n-nodes-langchain)
- Updated README badge with new n8n version
- Updated CHANGELOG with dependency changes

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-19 11:31:51 +01:00
Romuald Członkowski
5575630711 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
2025-11-18 17:41:17 +01:00
7 changed files with 1142 additions and 765 deletions

View File

@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.22.20] - 2025-11-19
### 🔄 Dependencies
**n8n Update to 1.120.3**
Updated all n8n-related dependencies to their latest versions:
- n8n: 1.119.1 → 1.120.3
- n8n-core: 1.118.0 → 1.119.2
- n8n-workflow: 1.116.0 → 1.117.0
- @n8n/n8n-nodes-langchain: 1.118.0 → 1.119.1
- Rebuilt node database with 544 nodes (439 from n8n-nodes-base, 105 from @n8n/n8n-nodes-langchain)
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.22.18] - 2025-11-14
### ✨ Features

View File

@@ -5,7 +5,7 @@
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp)
[![Tests](https://img.shields.io/badge/tests-3336%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions)
[![n8n version](https://img.shields.io/badge/n8n-1.119.1-orange.svg)](https://github.com/n8n-io/n8n)
[![n8n version](https://img.shields.io/badge/n8n-1.120.3-orange.svg)](https://github.com/n8n-io/n8n)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
@@ -1114,6 +1114,13 @@ Current database coverage (n8n v1.117.2):
## 🔄 Recent Updates
### v2.22.19 - Critical Bug Fix
**Fixed:** Stack overflow in session removal (Issue #427)
- Eliminated infinite recursion in HTTP server session cleanup
- Transport resources now deleted before closing to prevent circular event handler chain
- Production logs no longer show "RangeError: Maximum call stack size exceeded"
- All session cleanup operations now complete successfully without crashes
See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history and recent changes.
## ⚠️ Known Issues

Binary file not shown.

1783
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.22.18",
"version": "2.22.20",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -140,15 +140,15 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.1",
"@n8n/n8n-nodes-langchain": "^1.118.0",
"@n8n/n8n-nodes-langchain": "^1.119.1",
"@supabase/supabase-js": "^2.57.4",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"express-rate-limit": "^7.1.5",
"lru-cache": "^11.2.1",
"n8n": "^1.119.1",
"n8n-core": "^1.118.0",
"n8n-workflow": "^1.116.0",
"n8n": "^1.120.3",
"n8n-core": "^1.119.2",
"n8n-workflow": "^1.117.0",
"openai": "^4.77.0",
"sql.js": "^1.13.0",
"tslib": "^2.6.2",

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

View File

@@ -411,17 +411,17 @@ describe('HTTP Server Session Management', () => {
it('should handle removeSession with transport close error gracefully', async () => {
server = new SingleSessionHTTPServer();
const mockTransport = {
const mockTransport = {
close: vi.fn().mockRejectedValue(new Error('Transport close failed'))
};
(server as any).transports = { 'test-session': mockTransport };
(server as any).servers = { 'test-session': {} };
(server as any).sessionMetadata = {
'test-session': {
(server as any).sessionMetadata = {
'test-session': {
lastAccess: new Date(),
createdAt: new Date()
}
}
};
// Should not throw even if transport close fails
@@ -429,11 +429,67 @@ describe('HTTP Server Session Management', () => {
// Verify transport close was attempted
expect(mockTransport.close).toHaveBeenCalled();
// Session should still be cleaned up despite transport error
// Note: The actual implementation may handle errors differently, so let's verify what we can
expect(mockTransport.close).toHaveBeenCalledWith();
});
it('should not cause infinite recursion when transport.close triggers onclose handler', async () => {
server = new SingleSessionHTTPServer();
const sessionId = 'test-recursion-session';
let closeCallCount = 0;
let oncloseCallCount = 0;
// Create a mock transport that simulates the actual behavior
const mockTransport = {
close: vi.fn().mockImplementation(async () => {
closeCallCount++;
// Simulate the actual SDK behavior: close() triggers onclose handler
if (mockTransport.onclose) {
oncloseCallCount++;
await mockTransport.onclose();
}
}),
onclose: null as (() => Promise<void>) | null,
sessionId
};
// Set up the transport and session data
(server as any).transports = { [sessionId]: mockTransport };
(server as any).servers = { [sessionId]: {} };
(server as any).sessionMetadata = {
[sessionId]: {
lastAccess: new Date(),
createdAt: new Date()
}
};
// Set up onclose handler like the real implementation does
// This handler calls removeSession, which could cause infinite recursion
mockTransport.onclose = async () => {
await (server as any).removeSession(sessionId, 'transport_closed');
};
// Call removeSession - this should NOT cause infinite recursion
await (server as any).removeSession(sessionId, 'manual_removal');
// Verify the fix works:
// 1. close() should be called exactly once
expect(closeCallCount).toBe(1);
// 2. onclose handler should be triggered
expect(oncloseCallCount).toBe(1);
// 3. Transport should be deleted and not cause second close attempt
expect((server as any).transports[sessionId]).toBeUndefined();
expect((server as any).servers[sessionId]).toBeUndefined();
expect((server as any).sessionMetadata[sessionId]).toBeUndefined();
// 4. If there was a recursion bug, closeCallCount would be > 1
// or the test would timeout/crash with "Maximum call stack size exceeded"
});
});
describe('Session Metadata Tracking', () => {