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

  1. Remove `url` from `nodeClientOptions`; let Sentinel pick endpoints from the master name and root nodes.
  2. Move authentication and socket settings (password, TLS, family) into `nodeClientOptions` but never `url`.
  3. 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

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


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)