Skip to content

fix(client): bring disableClientInfo option back #2959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 88 additions & 43 deletions packages/client/lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP
import RedisCommandsQueue, { CommandOptions } from './commands-queue';
import { EventEmitter } from 'node:events';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors';
import { ClientClosedError, ClientOfflineError, DisconnectsClientError, SimpleError, WatchError } from '../errors';
import { URL } from 'node:url';
import { TcpSocketConnectOpts } from 'node:net';
import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply, TransformReply } from '../RESP/types';
import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply, TransformReply, CommandArguments } from '../RESP/types';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
import HELLO, { HelloOptions } from '../commands/HELLO';
Expand All @@ -19,6 +19,7 @@ import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../comm
import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache';
import { BasicCommandParser, CommandParser } from './parser';
import SingleEntryCache from '../single-entry-cache';
import { version } from '../../package.json'

export interface RedisClientOptions<
M extends RedisModules = RedisModules,
Expand Down Expand Up @@ -135,6 +136,14 @@ export interface RedisClientOptions<
* ```
*/
clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig;
/**
* If set to true, disables sending client identifier (user-agent like message) to the redis server
*/
disableClientInfo?: boolean;
/**
* Tag to append to library name that is sent to the Redis server
*/
clientInfoTag?: string;
}

type WithCommands<
Expand Down Expand Up @@ -514,7 +523,28 @@ export default class RedisClient<
});
}

async #handshake(selectedDB: number) {
async #handshake(chainId: symbol, asap: boolean) {
const promises = [];
const commandsWithErrorHandlers = await this.#getHandshakeCommands();

if (asap) commandsWithErrorHandlers.reverse()

for (const { cmd, errorHandler } of commandsWithErrorHandlers) {
promises.push(
this.#queue
.addCommand(cmd, {
chainId,
asap
})
.catch(errorHandler)
);
}
return promises;
}

async #getHandshakeCommands(): Promise<
Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }>
> {
const commands = [];
const cp = this.#options?.credentialsProvider;

Expand All @@ -532,8 +562,8 @@ export default class RedisClient<
}

if (cp && cp.type === 'streaming-credentials-provider') {

const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
const [credentials, disposable] =
await this.#subscribeForStreamingCredentials(cp);
this.#credentialsSubscription = disposable;

if (credentials.password) {
Expand All @@ -548,59 +578,88 @@ export default class RedisClient<
hello.SETNAME = this.#options.name;
}

commands.push(
parseArgs(HELLO, this.#options.RESP, hello)
);
commands.push({ cmd: parseArgs(HELLO, this.#options.RESP, hello) });
} else {

if (cp && cp.type === 'async-credentials-provider') {

const credentials = await cp.credentials();

if (credentials.username || credentials.password) {
commands.push(
parseArgs(COMMANDS.AUTH, {
commands.push({
cmd: parseArgs(COMMANDS.AUTH, {
username: credentials.username,
password: credentials.password ?? ''
})
);
});
}
}

if (cp && cp.type === 'streaming-credentials-provider') {

const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
const [credentials, disposable] =
await this.#subscribeForStreamingCredentials(cp);
this.#credentialsSubscription = disposable;

if (credentials.username || credentials.password) {
commands.push(
parseArgs(COMMANDS.AUTH, {
commands.push({
cmd: parseArgs(COMMANDS.AUTH, {
username: credentials.username,
password: credentials.password ?? ''
})
);
});
}
}

if (this.#options?.name) {
commands.push(
parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name)
);
commands.push({
cmd: parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name)
});
}
}

if (selectedDB !== 0) {
commands.push(['SELECT', this.#selectedDB.toString()]);
if (this.#selectedDB !== 0) {
commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] });
}

if (this.#options?.readonly) {
commands.push(
parseArgs(COMMANDS.READONLY)
);
commands.push({ cmd: parseArgs(COMMANDS.READONLY) });
}

if (!this.#options?.disableClientInfo) {
commands.push({
cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version],
errorHandler: (err: Error) => {
// Only throw if not a SimpleError - unknown subcommand
// Client libraries are expected to ignore failures
// of type SimpleError - unknown subcommand, which are
// expected from older servers ( < v7 )
if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) {
throw err;
}
}
});

commands.push({
cmd: [
'CLIENT',
'SETINFO',
'LIB-NAME',
this.#options?.clientInfoTag
? `node-redis(${this.#options.clientInfoTag})`
: 'node-redis'
],
errorHandler: (err: Error) => {
// Only throw if not a SimpleError - unknown subcommand
// Client libraries are expected to ignore failures
// of type SimpleError - unknown subcommand, which are
// expected from older servers ( < v7 )
if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) {
throw err;
}
}
});
}

if (this.#clientSideCache) {
commands.push(this.#clientSideCache.trackingOn());
commands.push({cmd: this.#clientSideCache.trackingOn()});
}

return commands;
Expand Down Expand Up @@ -629,15 +688,7 @@ export default class RedisClient<
);
}

const commands = await this.#handshake(this.#selectedDB);
for (let i = commands.length - 1; i >= 0; --i) {
promises.push(
this.#queue.addCommand(commands[i], {
chainId,
asap: true
})
);
}
promises.push(...(await this.#handshake(chainId, true)));

if (promises.length) {
this.#write();
Expand Down Expand Up @@ -1221,13 +1272,7 @@ export default class RedisClient<
selectedDB = this._self.#options?.database ?? 0;
this._self.#credentialsSubscription?.dispose();
this._self.#credentialsSubscription = null;
for (const command of (await this._self.#handshake(selectedDB))) {
promises.push(
this._self.#queue.addCommand(command, {
chainId
})
);
}
promises.push(...(await this._self.#handshake(chainId, false)));
this._self.#scheduleWrite();
await Promise.all(promises);
this._self.#selectedDB = selectedDB;
Expand Down
86 changes: 86 additions & 0 deletions packages/client/lib/commands/CLIENT_INFO.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert';
import CLIENT_INFO from './CLIENT_INFO';
import testUtils, { GLOBAL } from '../test-utils';
import { parseArgs } from './generic-transformers';
import { version } from '../../package.json';

describe('CLIENT INFO', () => {
testUtils.isVersionGreaterThanHook([6, 2]);
Expand Down Expand Up @@ -48,4 +49,89 @@ describe('CLIENT INFO', () => {
}
}
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('client.clientInfo Redis < 7', async client => {
const reply = await client.clientInfo();
if (!testUtils.isVersionGreaterThan([7])) {
assert.strictEqual(reply.libName, undefined, 'LibName should be undefined for Redis < 7');
assert.strictEqual(reply.libVer, undefined, 'LibVer should be undefined for Redis < 7');
}
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 info disabled', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, '');
assert.equal(reply.libVer, '');
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
disableClientInfo: true
}
});

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag set', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, 'node-redis(client1)');
assert.equal(reply.libVer, version);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
clientInfoTag: 'client1'
}
});

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp unset, info enabled, tag unset', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, 'node-redis');
assert.equal(reply.libVer, version);
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info enabled', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, 'node-redis(client1)');
assert.equal(reply.libVer, version);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
RESP: 2,
clientInfoTag: 'client1'
}
});

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp2 info disabled', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, '');
assert.equal(reply.libVer, '');
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
disableClientInfo: true,
RESP: 2
}
});

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info enabled', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, 'node-redis(client1)');
assert.equal(reply.libVer, version);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
RESP: 3,
clientInfoTag: 'client1'
}
});

testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.clientInfo Redis>=7 resp3 info disabled', async client => {
const reply = await client.clientInfo();
assert.equal(reply.libName, '');
assert.equal(reply.libVer, '');
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
disableClientInfo: true,
RESP: 3
}
});

});
13 changes: 11 additions & 2 deletions packages/client/lib/commands/CLIENT_INFO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export interface ClientInfoReply {
* available since 7.0
*/
resp?: number;
/**
* available since 7.0
*/
libName?: string;
/**
* available since 7.0
*/
libVer?: string;
}

const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g;
Expand All @@ -67,7 +75,6 @@ export default {
for (const item of rawReply.toString().matchAll(CLIENT_INFO_REGEX)) {
map[item[1]] = item[2];
}

const reply: ClientInfoReply = {
id: Number(map.id),
addr: map.addr,
Expand All @@ -89,7 +96,9 @@ export default {
totMem: Number(map['tot-mem']),
events: map.events,
cmd: map.cmd,
user: map.user
user: map.user,
libName: map['lib-name'],
libVer: map['lib-ver']
};

if (map.laddr !== undefined) {
Expand Down
6 changes: 5 additions & 1 deletion packages/client/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ export class ErrorReply extends Error {
}
}

export class SimpleError extends ErrorReply {}
export class SimpleError extends ErrorReply {
isUnknownSubcommand(): boolean {
return this.message.toLowerCase().indexOf('err unknown subcommand') !== -1;
}
}

export class BlobError extends ErrorReply {}

Expand Down
7 changes: 4 additions & 3 deletions packages/client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
"outDir": "./dist",
},
"include": [
"./index.ts",
"./lib/**/*.ts"
"./lib/**/*.ts",
"./package.json"
],
"exclude": [
"./lib/test-utils.ts",
Expand All @@ -18,6 +19,6 @@
"./lib"
],
"entryPointStrategy": "expand",
"out": "../../documentation/client"
"out": "../../documentation/client",
}
}
3 changes: 2 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"allowJs": true
"allowJs": true,
"resolveJsonModule": true
}
}
Loading