1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

fix(client): bring disableClientInfo option back (#2959)

* fix(client): bring disableClientInfo option back

It disappeared in v5

fixes #2958
This commit is contained in:
Nikolay Karadzhov
2025-05-20 15:15:09 +03:00
committed by GitHub
parent f3d1d3352e
commit 4a5f879ec9
6 changed files with 196 additions and 50 deletions

View File

@@ -4,11 +4,11 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP
import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import RedisCommandsQueue, { CommandOptions } from './commands-queue';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; 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 { URL } from 'node:url';
import { TcpSocketConnectOpts } from 'node:net'; import { TcpSocketConnectOpts } from 'node:net';
import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; 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 RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command'; import { RedisMultiQueuedCommand } from '../multi-command';
import HELLO, { HelloOptions } from '../commands/HELLO'; import HELLO, { HelloOptions } from '../commands/HELLO';
@@ -19,6 +19,7 @@ import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../comm
import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache'; import { BasicClientSideCache, ClientSideCacheConfig, ClientSideCacheProvider } from './cache';
import { BasicCommandParser, CommandParser } from './parser'; import { BasicCommandParser, CommandParser } from './parser';
import SingleEntryCache from '../single-entry-cache'; import SingleEntryCache from '../single-entry-cache';
import { version } from '../../package.json'
export interface RedisClientOptions< export interface RedisClientOptions<
M extends RedisModules = RedisModules, M extends RedisModules = RedisModules,
@@ -135,6 +136,14 @@ export interface RedisClientOptions<
* ``` * ```
*/ */
clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig; 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< type WithCommands<
@@ -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 commands = [];
const cp = this.#options?.credentialsProvider; const cp = this.#options?.credentialsProvider;
@@ -532,8 +562,8 @@ export default class RedisClient<
} }
if (cp && cp.type === 'streaming-credentials-provider') { if (cp && cp.type === 'streaming-credentials-provider') {
const [credentials, disposable] =
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) await this.#subscribeForStreamingCredentials(cp);
this.#credentialsSubscription = disposable; this.#credentialsSubscription = disposable;
if (credentials.password) { if (credentials.password) {
@@ -548,59 +578,88 @@ export default class RedisClient<
hello.SETNAME = this.#options.name; hello.SETNAME = this.#options.name;
} }
commands.push( commands.push({ cmd: parseArgs(HELLO, this.#options.RESP, hello) });
parseArgs(HELLO, this.#options.RESP, hello)
);
} else { } else {
if (cp && cp.type === 'async-credentials-provider') { if (cp && cp.type === 'async-credentials-provider') {
const credentials = await cp.credentials(); const credentials = await cp.credentials();
if (credentials.username || credentials.password) { if (credentials.username || credentials.password) {
commands.push( commands.push({
parseArgs(COMMANDS.AUTH, { cmd: parseArgs(COMMANDS.AUTH, {
username: credentials.username, username: credentials.username,
password: credentials.password ?? '' password: credentials.password ?? ''
}) })
); });
} }
} }
if (cp && cp.type === 'streaming-credentials-provider') { if (cp && cp.type === 'streaming-credentials-provider') {
const [credentials, disposable] =
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) await this.#subscribeForStreamingCredentials(cp);
this.#credentialsSubscription = disposable; this.#credentialsSubscription = disposable;
if (credentials.username || credentials.password) { if (credentials.username || credentials.password) {
commands.push( commands.push({
parseArgs(COMMANDS.AUTH, { cmd: parseArgs(COMMANDS.AUTH, {
username: credentials.username, username: credentials.username,
password: credentials.password ?? '' password: credentials.password ?? ''
}) })
); });
} }
} }
if (this.#options?.name) { if (this.#options?.name) {
commands.push( commands.push({
parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name) cmd: parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name)
); });
} }
} }
if (selectedDB !== 0) { if (this.#selectedDB !== 0) {
commands.push(['SELECT', this.#selectedDB.toString()]); commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] });
} }
if (this.#options?.readonly) { if (this.#options?.readonly) {
commands.push( commands.push({ cmd: parseArgs(COMMANDS.READONLY) });
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) { if (this.#clientSideCache) {
commands.push(this.#clientSideCache.trackingOn()); commands.push({cmd: this.#clientSideCache.trackingOn()});
} }
return commands; return commands;
@@ -629,15 +688,7 @@ export default class RedisClient<
); );
} }
const commands = await this.#handshake(this.#selectedDB); promises.push(...(await this.#handshake(chainId, true)));
for (let i = commands.length - 1; i >= 0; --i) {
promises.push(
this.#queue.addCommand(commands[i], {
chainId,
asap: true
})
);
}
if (promises.length) { if (promises.length) {
this.#write(); this.#write();
@@ -1221,13 +1272,7 @@ export default class RedisClient<
selectedDB = this._self.#options?.database ?? 0; selectedDB = this._self.#options?.database ?? 0;
this._self.#credentialsSubscription?.dispose(); this._self.#credentialsSubscription?.dispose();
this._self.#credentialsSubscription = null; this._self.#credentialsSubscription = null;
for (const command of (await this._self.#handshake(selectedDB))) { promises.push(...(await this._self.#handshake(chainId, false)));
promises.push(
this._self.#queue.addCommand(command, {
chainId
})
);
}
this._self.#scheduleWrite(); this._self.#scheduleWrite();
await Promise.all(promises); await Promise.all(promises);
this._self.#selectedDB = selectedDB; this._self.#selectedDB = selectedDB;

View File

@@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert';
import CLIENT_INFO from './CLIENT_INFO'; import CLIENT_INFO from './CLIENT_INFO';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { parseArgs } from './generic-transformers'; import { parseArgs } from './generic-transformers';
import { version } from '../../package.json';
describe('CLIENT INFO', () => { describe('CLIENT INFO', () => {
testUtils.isVersionGreaterThanHook([6, 2]); testUtils.isVersionGreaterThanHook([6, 2]);
@@ -48,4 +49,89 @@ describe('CLIENT INFO', () => {
} }
} }
}, GLOBAL.SERVERS.OPEN); }, 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
}
});
}); });

View File

@@ -52,6 +52,14 @@ export interface ClientInfoReply {
* available since 7.0 * available since 7.0
*/ */
resp?: number; resp?: number;
/**
* available since 7.0
*/
libName?: string;
/**
* available since 7.0
*/
libVer?: string;
} }
const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g;
@@ -67,7 +75,6 @@ export default {
for (const item of rawReply.toString().matchAll(CLIENT_INFO_REGEX)) { for (const item of rawReply.toString().matchAll(CLIENT_INFO_REGEX)) {
map[item[1]] = item[2]; map[item[1]] = item[2];
} }
const reply: ClientInfoReply = { const reply: ClientInfoReply = {
id: Number(map.id), id: Number(map.id),
addr: map.addr, addr: map.addr,
@@ -89,7 +96,9 @@ export default {
totMem: Number(map['tot-mem']), totMem: Number(map['tot-mem']),
events: map.events, events: map.events,
cmd: map.cmd, cmd: map.cmd,
user: map.user user: map.user,
libName: map['lib-name'],
libVer: map['lib-ver']
}; };
if (map.laddr !== undefined) { if (map.laddr !== undefined) {

View File

@@ -70,7 +70,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 {} export class BlobError extends ErrorReply {}

View File

@@ -1,11 +1,12 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist" "outDir": "./dist",
}, },
"include": [ "include": [
"./index.ts", "./index.ts",
"./lib/**/*.ts" "./lib/**/*.ts",
"./package.json"
], ],
"exclude": [ "exclude": [
"./lib/test-utils.ts", "./lib/test-utils.ts",
@@ -18,6 +19,6 @@
"./lib" "./lib"
], ],
"entryPointStrategy": "expand", "entryPointStrategy": "expand",
"out": "../../documentation/client" "out": "../../documentation/client",
} }
} }

View File

@@ -15,6 +15,7 @@
"sourceMap": true, "sourceMap": true,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"allowJs": true "allowJs": true,
"resolveJsonModule": true
} }
} }