Skip to content

Commit dc4f1f7

Browse files
nkaradzhovclaude
andcommitted
fix(client): fall back to default policy for unknown cluster commands
_executeWithPolicies routed every command through policy resolution, but the cluster's resolver (StaticPolicyResolver(POLICIES)) has no fallback, so any command absent from the policy table resolved to { ok: false } and the engine threw "Policy resolution error". This broke user-defined custom commands, scripts/functions whose dispatched name is not in the table, and module commands the resolver was not built with. Such commands have no request/response policy and nothing to split or aggregate. Instead of throwing, fall back to a default policy (default_keyed when the parser carries keys, else default_keyless) and route through the existing pass-through default router/reducer — the same single-client, key-routed behaviour as a direct _execute. Known module commands the dynamic resolver does recognise still get their real policy, so a module declaring multi_shard keeps working. Known commands that cannot be split still throw from the splitter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d1ccf7e commit dc4f1f7

1 file changed

Lines changed: 24 additions & 10 deletions

File tree

packages/client/lib/cluster/index.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SingleEntryCache from '../single-entry-cache'
1616
import { publish, CHANNELS } from '../client/tracing';
1717
import { ClientIdentity, ClientRole, generateClusterClientId } from '../client/identity';
1818
import { DEFAULT_COMMAND_TIMEOUT } from '../defaults';
19-
import { POLICIES, PolicyResolver, StaticPolicyResolver } from './request-response-policies';
19+
import { POLICIES, PolicyResolver, StaticPolicyResolver, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS, type CommandPolicies } from './request-response-policies';
2020
import { REQUEST_ROUTERS, RESPONSE_REDUCERS } from './request-response-policies/dispatch';
2121

2222
export type ClusterTopologyRefreshOnReconnectionAttemptStrategy =
@@ -462,14 +462,28 @@ export default class RedisCluster<
462462
): Promise<T> {
463463
const policyResult = this._policyResolver.resolvePolicy(parser.commandIdentifier);
464464

465-
if(!policyResult.ok) {
466-
const { command, subcommand } = parser.commandIdentifier;
467-
const label = subcommand ? `${command} ${subcommand}` : command;
468-
throw new Error(`Policy resolution error for ${label}: ${policyResult.error}`);
469-
}
470-
471-
const requestPolicy = policyResult.value.request
472-
const responsePolicy = policyResult.value.response
465+
// Commands the resolver doesn't know — user-defined custom commands,
466+
// scripts/functions, modules absent from the policy table — have no
467+
// request/response policy and nothing to split or aggregate. Fall back to
468+
// the default key-routed path (single client by `firstKey`, sole reply
469+
// passed through) rather than failing. Scripts/functions are single-slot
470+
// by contract, so default-keyed is always correct for them. Known
471+
// multi_shard commands that can't be split still throw from the splitter.
472+
const hasKeys = parser.keys.length > 0;
473+
const policy: CommandPolicies = policyResult.ok
474+
? policyResult.value
475+
: {
476+
request: hasKeys
477+
? REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED
478+
: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS,
479+
response: hasKeys
480+
? RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED
481+
: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS,
482+
isKeyless: !hasKeys
483+
};
484+
485+
const requestPolicy = policy.request
486+
const responsePolicy = policy.response
473487

474488
// https://redis.io/docs/latest/develop/reference/command-tips
475489
const router = REQUEST_ROUTERS[requestPolicy];
@@ -482,7 +496,7 @@ export default class RedisCluster<
482496
this._slots as unknown as Parameters<typeof router>[0],
483497
parser,
484498
isReadonly,
485-
policyResult.value.keySpecs
499+
policy.keySpecs
486500
);
487501

488502
if (plan.length === 0) {

0 commit comments

Comments
 (0)