Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Only required if `FEDIFY_KV_STORE_TYPE` is set to `redis`

- `REDIS_HOST` - Redis server host
- `REDIS_PORT` - Redis server port
- `REDIS_CLUSTER` - Set to `false` to connect to a standalone (single-node) Redis server, e.g. a plain `redis:alpine` container
- Default: cluster mode
- `REDIS_TLS_CERT` - TLS certificate for Redis connection
- Only required if Redis is configured to use TLS

Expand Down
83 changes: 51 additions & 32 deletions src/configuration/registrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
asFunction,
asValue,
} from 'awilix';
import Redis from 'ioredis';
import Redis, { type Cluster } from 'ioredis';
import type { Knex } from 'knex';

import { KnexAccountRepository } from '@/account/account.repository.knex';
Expand Down Expand Up @@ -94,6 +94,55 @@ import { LocalStorageAdapter } from '@/storage/adapters/local-storage-adapter';
import { ImageProcessor } from '@/storage/image-processor';
import { ImageStorageService } from '@/storage/image-storage.service';

/**
* Create a Redis connection for use as Fedify's KvStore.
*
* Defaults to a Redis Cluster connection (the historical behaviour). Set
* `REDIS_CLUSTER=false` to connect to a standalone (single-node) Redis server,
* e.g. a plain `redis:alpine` container.
*/
export function createRedisConnection(logging: Logger): Redis | Cluster {
const host = process.env.REDIS_HOST || 'localhost';
const port = Number(process.env.REDIS_PORT) || 6379;
const tls = process.env.REDIS_TLS_CERT
Comment on lines +106 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate REDIS_PORT explicitly instead of silently defaulting on malformed values.

Line 106 currently uses Number(process.env.REDIS_PORT) || 6379; malformed values (for example, 638O) silently become 6379, masking misconfiguration and potentially connecting to the wrong endpoint.

Suggested fix
-    const host = process.env.REDIS_HOST || 'localhost';
-    const port = Number(process.env.REDIS_PORT) || 6379;
+    const host = process.env.REDIS_HOST || 'localhost';
+    const rawPort = process.env.REDIS_PORT;
+    const parsedPort = rawPort == null ? undefined : Number(rawPort);
+    if (
+        parsedPort !== undefined &&
+        (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
+    ) {
+        throw new Error(`Invalid REDIS_PORT: ${rawPort}`);
+    }
+    const port = parsedPort ?? 6379;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const port = Number(process.env.REDIS_PORT) || 6379;
const tls = process.env.REDIS_TLS_CERT
const host = process.env.REDIS_HOST || 'localhost';
const rawPort = process.env.REDIS_PORT;
const parsedPort = rawPort == null ? undefined : Number(rawPort);
if (
parsedPort !== undefined &&
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
) {
throw new Error(`Invalid REDIS_PORT: ${rawPort}`);
}
const port = parsedPort ?? 6379;
const tls = process.env.REDIS_TLS_CERT
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/configuration/registrations.ts` around lines 106 - 107, The port
assignment at line 106 using Number(process.env.REDIS_PORT) || 6379 silently
converts malformed port values to NaN, which then defaults to 6379, masking
configuration errors. Implement explicit validation for the REDIS_PORT
environment variable that checks if the value is a valid number and within the
valid port range (1-65535), and throw an error with a clear message if the value
is invalid or missing, rather than allowing silent defaults on malformed input.

? { ca: process.env.REDIS_TLS_CERT }
: undefined;

const retryStrategy = (times: number) => {
const delay = Math.min(times * 50, 2000);
logging.warn(
`Redis connection retry attempt ${times}, delay ${delay}ms`,
);
return delay;
};

if (process.env.REDIS_CLUSTER === 'false') {
logging.info('Connecting to Redis in standalone mode');

return new Redis({
host,
port,
retryStrategy,
enableOfflineQueue: true,
maxRetriesPerRequest: 3,
enableReadyCheck: true,
tls,
});
}

logging.info('Connecting to Redis in cluster mode');

return new Redis.Cluster([{ host, port }], {
clusterRetryStrategy: retryStrategy,
enableOfflineQueue: true,
redisOptions: {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
tls,
},
});
}

export function registerDependencies(
container: AwilixContainer,
deps: {
Expand All @@ -116,38 +165,8 @@ export function registerDependencies(

if (kvStoreType === 'redis') {
logging.info('Using Redis KvStore for Fedify');
const host = process.env.REDIS_HOST || 'localhost';
const port = Number(process.env.REDIS_PORT) || 6379;

const redis = new Redis.Cluster(
[
{
host,
port,
},
],
{
clusterRetryStrategy: (times: number) => {
const delay = Math.min(times * 50, 2000);
logging.warn(
`Redis connection retry attempt ${times}, delay ${delay}ms`,
);
return delay;
},
enableOfflineQueue: true,
redisOptions: {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
tls: process.env.REDIS_TLS_CERT
? {
ca: process.env.REDIS_TLS_CERT,
}
: undefined,
},
},
);

return new RedisKvStore(redis);
return new RedisKvStore(createRedisConnection(logging));
}

logging.info('Using MySQL KvStore for Fedify');
Expand Down
90 changes: 90 additions & 0 deletions src/configuration/registrations.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { Logger } from '@logtape/logtape';

const clusterConstructor = vi.fn();
const redisConstructor = vi.fn();

vi.mock('ioredis', () => {
class Cluster {
constructor(...args: unknown[]) {
clusterConstructor(...args);
}
}
class Redis {
static Cluster = Cluster;
constructor(...args: unknown[]) {
redisConstructor(...args);
}
}
return { default: Redis, Cluster };
});

import { createRedisConnection } from './registrations';

describe('createRedisConnection', () => {
const logging = {
info: vi.fn(),
warn: vi.fn(),
} as unknown as Logger;

const originalEnv = { ...process.env };

beforeEach(() => {
clusterConstructor.mockClear();
redisConstructor.mockClear();
process.env.REDIS_HOST = 'redis-host';
process.env.REDIS_PORT = '6380';
delete process.env.REDIS_CLUSTER;
delete process.env.REDIS_TLS_CERT;
});

afterEach(() => {
process.env = { ...originalEnv };
});

it('connects in cluster mode by default', () => {
createRedisConnection(logging);

expect(clusterConstructor).toHaveBeenCalledTimes(1);
expect(redisConstructor).not.toHaveBeenCalled();

const nodes = clusterConstructor.mock.calls[0][0];
expect(nodes).toEqual([{ host: 'redis-host', port: 6380 }]);
});

it('connects in standalone mode when REDIS_CLUSTER is "false"', () => {
process.env.REDIS_CLUSTER = 'false';

createRedisConnection(logging);

expect(redisConstructor).toHaveBeenCalledTimes(1);
expect(clusterConstructor).not.toHaveBeenCalled();

const options = redisConstructor.mock.calls[0][0];
expect(options.host).toBe('redis-host');
expect(options.port).toBe(6380);
});

it('defaults to localhost:6379 when host/port are unset', () => {
process.env.REDIS_CLUSTER = 'false';
delete process.env.REDIS_HOST;
delete process.env.REDIS_PORT;

createRedisConnection(logging);

const options = redisConstructor.mock.calls[0][0];
expect(options.host).toBe('localhost');
expect(options.port).toBe(6379);
});

it('passes the TLS certificate through when configured', () => {
process.env.REDIS_CLUSTER = 'false';
process.env.REDIS_TLS_CERT = 'a-cert';

createRedisConnection(logging);

const options = redisConstructor.mock.calls[0][0];
expect(options.tls).toEqual({ ca: 'a-cert' });
});
});
Loading