Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code f7134d898c fix(session-storage): add error listener and reconnect strategy to Redis client
https://sonarly.com/issue/32619?type=bug

twenty-server crashes every time Redis closes an idle session-store connection because the node-redis client has no `error` event listener, turning a recoverable socket close into an uncaught exception that exits the process.

Fix: Three changes in `session-storage.module-factory.ts`:

1. **Added `redisClient.on('error', ...)` handler** (line 78-80) — this is the critical fix. Without it, any `error` event from node-redis (including `SocketClosedUnexpectedlyError` when Redis closes an idle connection) becomes an uncaught exception per Node.js EventEmitter semantics, crashing the process. The handler logs the error and allows node-redis's built-in reconnect to proceed.

2. **Added `reconnectStrategy` to socket options** (lines 65-75) — provides exponential backoff (500ms, 1s, 1.5s... capped at 5s) with warn-level logging on each attempt. This matches the resilience behavior of the `ioredis` client used by `RedisClientService` (which has `maxRetriesPerRequest: null` for infinite retry).

3. **Added `pingInterval: 30_000`** (line 64) — sends a PING every 30 seconds to keep the connection alive, preventing Redis from closing it due to idle timeout. This addresses the triggering cause (idle connections being closed) in addition to the proximate cause (unhandled error event).

4. **Replaced `throw new Error(...)` inside `.catch()` with `logger.error()`** (lines 82-86) — the original pattern threw inside a `.catch()` callback, creating a new rejected promise with no handler (an unhandled promise rejection). Now the error is logged properly without crashing.

Added `Logger` import from `@nestjs/common` and created a module-level `logger` instance following the team's convention for standalone factory functions (matching the pattern in `message-queue.explorer.ts` which uses `new Logger('MessageQueueModule')`).
2026-04-29 17:57:34 +00:00
@@ -1,5 +1,7 @@
import { createHash } from 'crypto';
import { Logger } from '@nestjs/common';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
@@ -8,6 +10,8 @@ import type session from 'express-session';
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
const logger = new Logger('SessionStorage');
export const getSessionStorageOptions = (
twentyConfigService: TwentyConfigService,
): session.SessionOptions => {
@@ -57,10 +61,28 @@ export const getSessionStorageOptions = (
const redisClient = createClient({
url: connectionString,
pingInterval: 30_000,
socket: {
reconnectStrategy: (retries: number) => {
const delay = Math.min(retries * 500, 5000);
logger.warn(
`Redis session storage reconnecting (attempt ${retries}, next retry in ${delay}ms)`,
);
return delay;
},
},
});
redisClient.on('error', (err) => {
logger.error(`Redis session storage error: ${err.message}`);
});
redisClient.connect().catch((err) => {
throw new Error(`Redis connection failed: ${err}`);
logger.error(
`Redis session storage initial connection failed: ${err.message}`,
);
});
return {