From 6ed1aba952df82a84d12e7d8c5c42a86bea75c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20=C3=87orlu?= Date: Fri, 27 Mar 2026 14:57:23 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20crash=20in=20Redis=20ada?= =?UTF-8?q?pter=20when=20storeConfig=20is=20not=20provided?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When storeConfig was omitted from the Redis cache adapter config, the retryStrategy callback would throw a TypeError whenever ioredis tried to reconnect to Redis, crashing Ghost instead of recovering gracefully. Fixed by using optional chaining on config.storeConfig. --- .../adapters/lib/redis/AdapterCacheRedis.js | 3 +- .../lib/redis/adapter-cache-redis.test.js | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/ghost/core/core/server/adapters/lib/redis/AdapterCacheRedis.js b/ghost/core/core/server/adapters/lib/redis/AdapterCacheRedis.js index 9a1177aefef..92cf2e2b659 100644 --- a/ghost/core/core/server/adapters/lib/redis/AdapterCacheRedis.js +++ b/ghost/core/core/server/adapters/lib/redis/AdapterCacheRedis.js @@ -13,6 +13,7 @@ class AdapterCacheRedis extends BaseCacheAdapter { * @param {Object} [config.cache] - caching instance compatible with cache-manager's redis store * @param {String} [config.host] - redis host used in case no cache instance provided * @param {Number} [config.port] - redis port used in case no cache instance provided + * @param {String} [config.username] - redis username used in case no cache instance provided * @param {String} [config.password] - redis password used in case no cache instance provided * @param {Object} [config.clusterConfig] - redis cluster config used in case no cache instance provided * @param {Object} [config.storeConfig] - extra redis client config used in case no cache instance provided @@ -46,7 +47,7 @@ class AdapterCacheRedis extends BaseCacheAdapter { username: config.username, password: config.password, retryStrategy: () => { - return (config.storeConfig.retryConnectSeconds || 10) * 1000; + return (config.storeConfig?.retryConnectSeconds || 10) * 1000; }, ...config.storeConfig, clusterConfig: config.clusterConfig diff --git a/ghost/core/test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js b/ghost/core/test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js index 167f7117f4b..d6378a5d41f 100644 --- a/ghost/core/test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js +++ b/ghost/core/test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js @@ -41,6 +41,40 @@ describe('Adapter Cache Redis', function () { assert.equal(cache.redisClient.options.retryStrategy, false); }); + describe('retryStrategy', function () { + it('does not throw and defaults to 10 seconds when storeConfig is not provided', function () { + const cache = new RedisCache({ + reuseConnection: false + }); + // retryStrategy is invoked by ioredis whenever Redis becomes unavailable. + // It must not crash even when storeConfig is omitted from the adapter config. + assert.doesNotThrow(() => cache.redisClient.options.retryStrategy(1)); + assert.equal(cache.redisClient.options.retryStrategy(1), 10000); + cache.redisClient.disconnect(); + }); + + it('defaults to 10 seconds when retryConnectSeconds is not set in storeConfig', function () { + const cache = new RedisCache({ + storeConfig: { + lazyConnect: true + }, + reuseConnection: false + }); + assert.equal(cache.redisClient.options.retryStrategy(1), 10000); + }); + + it('uses retryConnectSeconds from storeConfig when provided', function () { + const cache = new RedisCache({ + storeConfig: { + lazyConnect: true, + retryConnectSeconds: 5 + }, + reuseConnection: false + }); + assert.equal(cache.redisClient.options.retryStrategy(1), 5000); + }); + }); + describe('get', function () { it('can get a value from the cache', async function () { const redisCacheInstanceStub = {