From d70448f9fbe8b4bd8b97e114bd901f03d4e389ba Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 9 May 2025 18:40:46 +0300 Subject: [PATCH 1/2] fix(client): bring disableClientInfo option back It disappeared in v5 fixes #2958 --- packages/client/lib/client/index.ts | 131 ++++++++++++------ .../client/lib/commands/CLIENT_INFO.spec.ts | 86 ++++++++++++ packages/client/lib/commands/CLIENT_INFO.ts | 13 +- packages/client/lib/errors.ts | 6 +- packages/client/tsconfig.json | 7 +- tsconfig.base.json | 3 +- 6 files changed, 197 insertions(+), 49 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index c7f94fe680a..ac5f8cfd4d2 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -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'; @@ -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, @@ -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< @@ -514,7 +523,30 @@ export default class RedisClient< }); } - async #handshake(selectedDB: number) { + async #handshake(chainId: symbol, asap: boolean) { + const promises = []; + const commandsWithErrorHandlers = await this.#getHandshakeCommands(this.#selectedDB ?? 0); + + if (asap) commandsWithErrorHandlers.reverse() + + for (const { cmd, errorHandler } of commandsWithErrorHandlers) { + promises.push( + this.#queue + .addCommand(cmd, { + chainId, + asap + }) + .catch(errorHandler) + ); + } + return promises; + } + + async #getHandshakeCommands( + selectedDB: number + ): Promise< + Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }> + > { const commands = []; const cp = this.#options?.credentialsProvider; @@ -532,8 +564,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) { @@ -548,59 +580,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()]); + 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; @@ -629,15 +690,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(); @@ -1221,13 +1274,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; diff --git a/packages/client/lib/commands/CLIENT_INFO.spec.ts b/packages/client/lib/commands/CLIENT_INFO.spec.ts index 50345a46ce3..96881e6c1aa 100644 --- a/packages/client/lib/commands/CLIENT_INFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts @@ -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]); @@ -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 + } + }); + }); diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index 36dac175443..8908bdb2600 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -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; @@ -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, @@ -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) { diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index 8af4c5e5bed..74c261cc80e 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -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 {} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 8caa47300d4..b1f7b44d915 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -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", @@ -18,6 +19,6 @@ "./lib" ], "entryPointStrategy": "expand", - "out": "../../documentation/client" + "out": "../../documentation/client", } } diff --git a/tsconfig.base.json b/tsconfig.base.json index bd2bcac0845..d4a631fc008 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "sourceMap": true, "declaration": true, "declarationMap": true, - "allowJs": true + "allowJs": true, + "resolveJsonModule": true } } From a8cf1faa0d263a17636cbad03146da1efa439963 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 20 May 2025 15:00:06 +0300 Subject: [PATCH 2/2] remove redundant parameter --- packages/client/lib/client/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index ac5f8cfd4d2..8d98aa8ed26 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -525,7 +525,7 @@ export default class RedisClient< async #handshake(chainId: symbol, asap: boolean) { const promises = []; - const commandsWithErrorHandlers = await this.#getHandshakeCommands(this.#selectedDB ?? 0); + const commandsWithErrorHandlers = await this.#getHandshakeCommands(); if (asap) commandsWithErrorHandlers.reverse() @@ -542,9 +542,7 @@ export default class RedisClient< return promises; } - async #getHandshakeCommands( - selectedDB: number - ): Promise< + async #getHandshakeCommands(): Promise< Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }> > { const commands = []; @@ -617,7 +615,7 @@ export default class RedisClient< } } - if (selectedDB !== 0) { + if (this.#selectedDB !== 0) { commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] }); }