Skip to content

fix: ensure module commands respect proxy typeMapping #3261

Open
watersRand wants to merge 7 commits into
redis:masterfrom
watersRand:fix/withTypeMappings
Open

fix: ensure module commands respect proxy typeMapping #3261
watersRand wants to merge 7 commits into
redis:masterfrom
watersRand:fix/withTypeMappings

Conversation

@watersRand
Copy link
Copy Markdown
Contributor

@watersRand watersRand commented May 2, 2026

Description

Closes #3055 ,a bug where commands generated via static factories (Modules and Functions) were incorrectly referencing the root client's options (this._self._commandOptions) instead of the immediate instance's options (this._commandOptions).

The Problem:
When using .withCommandOptions() or .withTypeMapping(), the library creates a proxy object. However, because Modules and Functions were hard-coded to look at the internal _self reference for their configuration, they completely ignored any user-defined type mappings or timeouts set on the proxy. This effectively broke RESP3 type mapping for all Redis Module commands.

The Solution:

  • Updated #createModuleCommand and #createFunctionCommand to reference this._commandOptions.
  • Updated sendCommand to prioritize instance-level options.
  • Updated the NamespaceProxyClient type definition to include _commandOptions to ensure type safety.

Allows developers to seamlessly switch between different configurations—such as string vs. binary (Buffer) outputs—across all available commands, including built-in modules, third-party modules, and user-defined libraries (Redis Functions). It enables this flexibility on the fly without the overhead of re-initializing the entire connection or maintaining multiple client instances for different return types.


Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

Medium Risk
Touches core command dispatch and option merging for client, pool, cluster, and Sentinel; behavior change is intentional but affects all proxied commands.

Overview
Fixes command option inheritance for proxied clients so module, function, and related dispatch paths use the current instance’s _commandOptions instead of always reading the root client’s options via _self.

withCommandOptions / sendCommand on standalone clients and pools now layer options with Object.assign(Object.create(...)), so overrides like typeMapping apply while timeouts and other base settings stay inherited through the prototype chain rather than being dropped or fully replaced.

Namespace proxies (e.g. client.module) are cached per client instance and expose _commandOptions through a getter tied to _self, so updating base options or creating a typed proxy is reflected on module commands without stale snapshots.

The same instance-level _commandOptions wiring is applied across pool, cluster, and Sentinel command factories, with integration tests for buffer typeMapping and inherited timeouts.

Reviewed by Cursor Bugbot for commit 3b671da. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread packages/client/lib/client/index.ts
Comment thread packages/client/lib/client/index.ts Outdated
Comment thread packages/client/lib/client/index.spec.ts
Comment thread packages/client/lib/commander.ts Outdated
Comment thread packages/client/lib/commander.ts Outdated
@nkaradzhov
Copy link
Copy Markdown
Collaborator

@watersRand thanks, I will have a look!

Copy link
Copy Markdown
Collaborator

@nkaradzhov nkaradzhov left a comment

Choose a reason for hiding this comment

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

There are more places where this._self._commandOptions or this._self.commandOptions is used:

return this._self._executeCommand(command, parser, this._self._commandOptions, transformReply);

return this._self._executeCommand(fn, parser, this._self._commandOptions, transformReply);

...this._self._commandOptions,

return this._self.execute(client => client._executeCommand(command, parser, this._self._commandOptions, transformReply))

return this._self.execute(client => client._executeCommand(fn, parser, this._self._commandOptions, transformReply)) };

this._self._commandOptions,

this._self._commandOptions,

...this._self._commandOptions,

client => client._executeCommand(fn, parser, this._self.commandOptions, transformReply)

client => client._executeCommand(command, parser, this._self.commandOptions, transformReply)

It would be great if we can fix and test those as well.

});


testUtils.testWithClient('Module TypeMapping Fix', async client => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe change the title to: "proxies respect command options"

Comment thread packages/client/lib/commander.ts Outdated
Comment thread packages/client/lib/sentinel/utils.ts
Comment thread packages/client/lib/client/pool.ts
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 3b671da. Configure here.

.then(resolve, reject)
.finally(() => {
this.#returnClient(node);
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pool sendCommand ignores proxy command options

High Severity

The pool's sendCommand passes the call-site options directly to the underlying client without merging this._commandOptions. Unlike the client's and cluster's sendCommand (both updated in this PR to merge proxy options), the pool's version just does client.sendCommand(args, options). When bufferProxy.sendCommand(['ECHO', 'hello']) is called, options is undefined, and the raw pool client has no _commandOptions, so the proxy's typeMapping is never applied. The new test asserting bufferReply instanceof Buffer would fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b671da. Configure here.

OPTIONS extends ClusterCommandOptions<TYPE_MAPPING/*, CommandPolicies*/>,
TYPE_MAPPING extends TypeMapping,
// POLICIES extends CommandPolicies
// POLICIES extends CommandPolicies
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cluster withCommandOptions lacks prototype-chain inheritance for options

Medium Severity

The cluster's withCommandOptions sets proxy._commandOptions = options directly, unlike the client and pool which were updated in this PR to use Object.assign(Object.create(this._commandOptions ?? null), options). Since sendCommand and module commands were changed to read this._commandOptions (instead of this._self._commandOptions), chaining withCommandOptions calls on the cluster now silently drops parent options like timeout, whereas the same pattern on client/pool correctly inherits them via the prototype chain.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b671da. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

withTypeMappings not working

2 participants