redis/node-redis · error · Error
invalid nodeClientOptions for Sentinel
Error message
invalid nodeClientOptions for Sentinel
What it means
The Sentinel client discovers master/replica host:port pairs itself and injects them into each per-node `RedisClient` via `socket.host`/`socket.port`. A `url` field inside `nodeClientOptions` would name a fixed endpoint that conflicts with that discovery, so the constructor rejects it. The check runs only on the node (master/replica) options, not the sentinel-monitor options.
Solutions
- Remove `url` from `nodeClientOptions`; let Sentinel pick endpoints from the master name and root nodes.
- Move authentication and socket settings (password, TLS, family) into `nodeClientOptions` but never `url`.
- Provide master/replica endpoints through `rootNodes` / the sentinel master `name`, not through node options.
Example fix
// before
new RedisSentinel({ name: 'mymaster', nodeClientOptions: { url: 'redis://:secret@host:6379' } }, id);
// after
new RedisSentinel({ name: 'mymaster', rootNodes: [{ host: 'sentinel', port: 26379 }], nodeClientOptions: { password: 'secret' } }, id); Defensive patterns
Strategy: validation
Validate before calling
function assertNodeOpts(opts: { nodeClientOptions?: { url?: unknown } }) {
if (opts.nodeClientOptions && opts.nodeClientOptions.url !== undefined) {
throw new Error('nodeClientOptions.url is forbidden for Sentinel; use rootNodes');
}
}
assertNodeOpts(opts); Type guard
function hasNoNodeUrl(opts: { nodeClientOptions?: { url?: unknown } }): boolean {
return opts.nodeClientOptions?.url === undefined;
} Try / catch
try { new RedisSentinel(opts, id); }
catch (e) {
if (String(e).includes('invalid nodeClientOptions')) {
const { url, ...rest } = opts.nodeClientOptions ?? {};
opts = { ...opts, nodeClientOptions: rest };
new RedisSentinel(opts, id);
} else throw e;
} Prevention
- Never put a connection URL in nodeClientOptions; Sentinel owns endpoint selection.
- Run a lint-style assert on options before constructing Sentinel clients.
When it happens
Trigger: Passing `nodeClientOptions: { url: 'redis://...' }` (or any object whose `url` is not undefined) to `new RedisSentinel(...)`. Throws synchronously in the constructor.
Common situations: Reusing a `RedisClientOptions` literal that already carries a `url` for a standalone client; templating options from config that includes a connection URL; spreading a shared default options object.
Related errors
- invalid sentinelClientOptions for Sentinel
- Client Side Caching is only supported with RESP3
- Master Node Not Enumerated
- no replicas available for read
- already attempting to open
AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11).
Data as JSON: /api/errors/2faac772b75f4319.
Report an issue: GitHub.
Appendix: source
Thrown at packages/client/lib/sentinel/index.ts:864
this.#name = options.name;
this.#sentinelClientId = sentinelClientId;
this.#RESP = options.RESP;
this.#keyPrefix = options.keyPrefix;
this.#sentinelSeedNodes = Array.from(options.sentinelRootNodes);
// Initial root nodes start as a copy of the seed nodes; transform() later
// merges discovered nodes on top while preserving these seeds.
this.#sentinelRootNodes = Array.from(this.#sentinelSeedNodes);
this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16;
this.#masterPoolSize = options.masterPoolSize ?? 1;
this.#replicaPoolSize = options.replicaPoolSize ?? 0;
this.#nodeAddressMap = options.nodeAddressMap;
this.#scanInterval = options.scanInterval ?? 0;
this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false;
this.#nodeClientOptions = (options.nodeClientOptions ? {...options.nodeClientOptions} : {}) as RedisClientOptions<M, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
if (this.#nodeClientOptions.url !== undefined) {
throw new Error("invalid nodeClientOptions for Sentinel");
}
// One fieldset registry across master/replica node clients: fieldsets registered before
// a failover must be transparently re-preparable on the promoted master's connections.
// (Sentinel-monitor clients use #sentinelClientOptions and never run HIMPORT.)
this.#nodeClientOptions.himportRegistry = new FieldsetRegistry();
if (options.clientSideCache) {
if (options.clientSideCache instanceof PooledClientSideCacheProvider) {
this.#clientSideCache = this.#nodeClientOptions.clientSideCache = options.clientSideCache;
} else {
const cscConfig = options.clientSideCache;
this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig);
// this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig);
}
}
this.#sentinelClientOptions = options.sentinelClientOptions ? Object.assign({} as RedisClientOptions<typeof RedisSentinelModule, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>, options.sentinelClientOptions) : {};
this.#sentinelClientOptions.modules = RedisSentinelModule;View on GitHub (pinned to 90fd0652bc)