{"id":"5f79f25d636fce48","repo":"redis/node-redis","slug":"request-policy-requestpolicy-produced-no-target","errorCode":null,"errorMessage":"Request policy ${requestPolicy} produced no target nodes","messagePattern":"Request policy (.+?) produced no target nodes","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/cluster/index.ts","lineNumber":589,"sourceCode":"    }\n    // Validated before any per-node promise is dispatched — throwing after\n    // would orphan in-flight rejections (unhandled-rejection noise) and run\n    // side effects for a reply that can never be reduced.\n    const reducer = RESPONSE_REDUCERS[responsePolicy];\n    if (!reducer) {\n      throw new Error(`Unknown response policy ${responsePolicy}`);\n    }\n    // Routers are typed against the erased base cluster types (routing is\n    // below the typed command surface); bridge this instantiation's slots in.\n    const plan = await router(\n      this._slots as unknown as Parameters<typeof router>[0],\n      parser,\n      readonly,\n      policy.keySpecs\n    );\n\n    if (plan.length === 0) {\n      throw new Error(`Request policy ${requestPolicy} produced no target nodes`);\n    }\n\n    // Numeric aggregation must see raw numbers: strip the caller's type\n    // mapping from the per-node executions (a `NUMBER: String` mapping would\n    // feed strings into the numeric reducers) and re-apply it to the\n    // aggregated result below. Pass-through policies keep the mapping — their\n    // replies reach the caller undisturbed.\n    const numericAgg = NUMERIC_AGG_POLICIES.has(responsePolicy);\n    const requestedMapping = options?.typeMapping;\n    const execOptions = numericAgg && requestedMapping\n      ? { ...options, typeMapping: undefined }\n      : options;\n\n    // Track the actually-serving client for single-target plans: after a\n    // MOVED/ASK redirect the reply comes from a different node than\n    // `plan[0].client`, and the post-reply hooks must bind cursors to the node\n    // that really served. Multi-target hooks are no-ops, so nothing to track.\n    const served: { client?: RedisClientType<M, F, S, RESP, TYPE_MAPPING> } = {};","sourceCodeStart":571,"sourceCodeEnd":607,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/cluster/index.ts#L571-L607","documentation":"Thrown after a request-policy router runs but returns an empty plan (zero target nodes). The fan-out routers build the plan from the live slots topology, so an empty plan means RedisClusterSlots currently knows of no usable nodes. The client refuses to dispatch a fan-out command to nothing rather than silently returning an empty/trivial aggregate. This is a topology-state precondition, not a bug in the command itself.","triggerScenarios":"Issuing a command whose request policy is `all_nodes` or `all_shards` (e.g. SCRIPT KILL under one_succeeded, or any all-shards fan-out) when `RedisClusterSlots.getAllNodes()`/`getAllMasterNodes()` returns []. Also reachable if a rediscovery cycle evicted every node and has not repopulated, or a command is issued against a cluster that was never connected or has been disconnected.","commonSituations":"Awaiting a fan-out command before `await cluster.connect()` resolves; starting the cluster against a seed list where every seed is down; issuing commands after `cluster.disconnect()`; a topology refresh racing during a full cluster outage where the slot map is transiently empty.","solutions":["Ensure `await cluster.connect()` has resolved before issuing any command, especially fan-out ones.","Verify at least one seed node in `rootNodes`/`nodeAddressMap` is reachable and is a cluster master.","If hit mid-run, retry after a short delay to let `rediscover` repopulate the slot map from a live seed.","Confirm the client instance is not reused after `.disconnect()`/`.close()`."],"exampleFix":"// before\nconst cluster = createCluster({ rootNodes: [{ socket: { host: '127.0.0.1', port: 7000 } }] });\nawait cluster.sendCommand(['SCRIPT', 'KILL']); // throws: produced no target nodes\n\n// after\nawait cluster.connect();\nawait cluster.sendCommand(['SCRIPT', 'KILL']);","handlingStrategy":"validation","validationCode":"// Guard fan-out commands against an empty topology before dispatch.\nasync function assertClusterReady(cluster) {\n  if (!cluster.isOpen) await cluster.connect();\n  // trigger a cheap topology check via a keyless round-trip\n  await cluster.sendCommand(['PING']);\n}\n\nawait assertClusterReady(cluster);\nawait cluster.sendCommand(['SCRIPT', 'KILL']);","typeGuard":null,"tryCatchPattern":"try {\n  await cluster.sendCommand(['SCRIPT', 'KILL']);\n} catch (e) {\n  if (/produced no target nodes/.test(e.message)) {\n    // topology not populated — reconnect or wait for rediscovery\n    await cluster.connect();\n    throw e; // surface to caller for retry decision\n  }\n  throw e;\n}","preventionTips":["Always `await cluster.connect()` before issuing commands and treat it as a precondition.","Do not reuse a cluster client after disconnect()/close().","For fan-out (all_nodes/all_shards) commands, ensure the cluster has at least one reachable master.","Wire a top-level 'not connected' guard in your data-access layer."],"tags":["cluster","topology","routing","startup","all-shards"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}