Skip to content

Commit 38d7b56

Browse files
authored
fix redis#1846 - handle arguments that are not buffers or strings (redis#1849)
* fix redis#1846 - handle arguments that are not buffers or strings * use toString() instead of throw TypeError * remove .only and uncomment tests
1 parent e7a8213 commit 38d7b56

File tree

7 files changed

+59
-68
lines changed

7 files changed

+59
-68
lines changed

packages/client/lib/client/commands-queue.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface CommandWaitingToBeSent extends CommandWaitingForReply {
2626

2727
interface CommandWaitingForReply {
2828
resolve(reply?: unknown): void;
29-
reject(err: Error): void;
29+
reject(err: unknown): void;
3030
channelsCounter?: number;
3131
returnBuffers?: boolean;
3232
}
@@ -142,7 +142,9 @@ export default class RedisCommandsQueue {
142142
},
143143
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
144144
});
145+
145146
#chainInExecution: symbol | undefined;
147+
146148
constructor(maxLength: number | null | undefined) {
147149
this.#maxLength = maxLength;
148150
}
@@ -155,6 +157,7 @@ export default class RedisCommandsQueue {
155157
} else if (options?.signal?.aborted) {
156158
return Promise.reject(new AbortError());
157159
}
160+
158161
return new Promise((resolve, reject) => {
159162
const node = new LinkedList.Node<CommandWaitingToBeSent>({
160163
args,
@@ -178,6 +181,7 @@ export default class RedisCommandsQueue {
178181
once: true
179182
});
180183
}
184+
181185
if (options?.asap) {
182186
this.#waitingToBeSent.unshiftNode(node);
183187
} else {
@@ -386,12 +390,13 @@ export default class RedisCommandsQueue {
386390

387391
flushWaitingForReply(err: Error): void {
388392
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
389-
if (!this.#chainInExecution) {
390-
return;
391-
}
393+
394+
if (!this.#chainInExecution) return;
395+
392396
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
393397
this.#waitingToBeSent.shift();
394398
}
399+
395400
this.#chainInExecution = undefined;
396401
}
397402

packages/client/lib/client/index.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { strict as assert } from 'assert';
22
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
3-
import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
3+
import RedisClient, { RedisClientType } from '.';
44
import { RedisClientMultiCommandType } from './multi-command';
55
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
66
import { AbortError, AuthError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
@@ -183,7 +183,7 @@ describe('Client', () => {
183183
}
184184
});
185185

186-
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
186+
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: Array<any>): Promise<RedisCommandRawReply> {
187187
return new Promise((resolve, reject) => {
188188
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
189189
if (err) return reject(err);
@@ -353,6 +353,18 @@ describe('Client', () => {
353353
);
354354
}, GLOBAL.SERVERS.OPEN);
355355
});
356+
357+
testUtils.testWithClient('undefined and null should not break the client', async client => {
358+
await assert.rejects(
359+
client.sendCommand([null as any, undefined as any]),
360+
'ERR unknown command ``, with args beginning with: ``'
361+
);
362+
363+
assert.equal(
364+
await client.ping(),
365+
'PONG'
366+
);
367+
}, GLOBAL.SERVERS.OPEN);
356368
});
357369

358370
describe('multi', () => {

packages/client/lib/client/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CommandOptions, commandOptions, isCommandOptions } from '../command-opt
99
import { ScanOptions, ZMember } from '../commands/generic-transformers';
1010
import { ScanCommandOptions } from '../commands/SCAN';
1111
import { HScanTuple } from '../commands/HSCAN';
12-
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
12+
import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply } from '../commander';
1313
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
1414
import { ClientClosedError, DisconnectsClientError, AuthError } from '../errors';
1515
import { URL } from 'url';
@@ -83,7 +83,6 @@ export interface ClientCommandOptions extends QueueCommandOptions {
8383

8484
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
8585

86-
export type ClientLegacyCommandArguments = LegacyCommandArguments | [...LegacyCommandArguments, ClientLegacyCallback];
8786
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
8887
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
8988
return commandOptions(options);
@@ -292,13 +291,13 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
292291
if (!this.#options?.legacyMode) return;
293292

294293
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
295-
(this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
294+
(this as any).sendCommand = (...args: Array<any>): void => {
296295
let callback: ClientLegacyCallback;
297296
if (typeof args[args.length - 1] === 'function') {
298297
callback = args.pop() as ClientLegacyCallback;
299298
}
300299

301-
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
300+
this.#sendCommand(args.flat())
302301
.then((reply: RedisCommandRawReply) => {
303302
if (!callback) return;
304303

packages/client/lib/client/multi-command.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import COMMANDS from './commands';
22
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
33
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
4-
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformLegacyCommandArguments } from '../commander';
4+
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
55
import { ExcludeMappedString } from '.';
66

77
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
@@ -53,8 +53,8 @@ export default class RedisClientMultiCommand {
5353

5454
#legacyMode(): void {
5555
this.v4.addCommand = this.addCommand.bind(this);
56-
(this as any).addCommand = (...args: LegacyCommandArguments): this => {
57-
this.#multi.addCommand(transformLegacyCommandArguments(args));
56+
(this as any).addCommand = (...args: Array<any>): this => {
57+
this.#multi.addCommand(args.flat());
5858
return this;
5959
};
6060
this.v4.exec = this.exec.bind(this);

packages/client/lib/client/socket.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -249,18 +249,16 @@ export default class RedisSocket extends EventEmitter {
249249
#isCorked = false;
250250

251251
cork(): void {
252-
if (!this.#socket) {
252+
if (!this.#socket || this.#isCorked) {
253253
return;
254254
}
255255

256-
if (!this.#isCorked) {
257-
this.#socket.cork();
258-
this.#isCorked = true;
256+
this.#socket.cork();
257+
this.#isCorked = true;
259258

260-
queueMicrotask(() => {
261-
this.#socket?.uncork();
262-
this.#isCorked = false;
263-
});
264-
}
259+
queueMicrotask(() => {
260+
this.#socket?.uncork();
261+
this.#isCorked = false;
262+
});
265263
}
266264
}

packages/client/lib/commander.spec.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,34 @@ import { strict as assert } from 'assert';
22
import { describe } from 'mocha';
33
import { encodeCommand } from './commander';
44

5-
function encodeCommandToString(...args: Parameters<typeof encodeCommand>): string {
6-
const arr = [];
7-
for (const item of encodeCommand(...args)) {
8-
arr.push(item.toString());
9-
}
10-
11-
return arr.join('');
12-
}
135

146
describe('Commander', () => {
157
describe('encodeCommand (see #1628)', () => {
168
it('1 byte', () => {
17-
assert.equal(
18-
encodeCommandToString(['a', 'z']),
19-
'*2\r\n$1\r\na\r\n$1\r\nz\r\n'
9+
assert.deepEqual(
10+
[...encodeCommand(['a', 'z'])],
11+
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
2012
);
2113
});
2214

2315
it('2 bytes', () => {
24-
assert.equal(
25-
encodeCommandToString(['א', 'ת']),
26-
'*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
16+
assert.deepEqual(
17+
[...encodeCommand(['א', 'ת'])],
18+
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
2719
);
2820
});
2921

3022
it('4 bytes', () => {
31-
assert.equal(
32-
encodeCommandToString(['🐣', '🐤']),
33-
'*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
23+
assert.deepEqual(
24+
[...encodeCommand(['🐣', '🐤'])],
25+
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
3426
);
3527
});
3628

3729
it('with a buffer', () => {
38-
assert.equal(
39-
encodeCommandToString([Buffer.from('string')]),
40-
'*1\r\n$6\r\nstring\r\n'
30+
assert.deepEqual(
31+
[...encodeCommand([Buffer.from('string')])],
32+
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
4133
);
4234
});
4335
});

packages/client/lib/commander.ts

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,25 @@ export function* encodeCommand(args: RedisCommandArguments): IterableIterator<Re
9595
let strings = `*${args.length}${DELIMITER}`,
9696
stringsLength = 0;
9797
for (const arg of args) {
98-
const isString = typeof arg === 'string',
99-
byteLength = isString ? Buffer.byteLength(arg): arg.length;
100-
strings += `$${byteLength}${DELIMITER}`;
98+
if (Buffer.isBuffer(arg)) {
99+
yield `${strings}$${arg.length}${DELIMITER}`;
100+
strings = '';
101+
stringsLength = 0;
102+
yield arg;
103+
} else {
104+
const string = arg?.toString?.() ?? '',
105+
byteLength = Buffer.byteLength(string);
106+
strings += `$${byteLength}${DELIMITER}`;
101107

102-
if (isString) {
103108
const totalLength = stringsLength + byteLength;
104109
if (totalLength > 1024) {
105110
yield strings;
106-
strings = arg;
111+
strings = string;
107112
stringsLength = byteLength;
108113
} else {
109-
strings += arg;
114+
strings += string;
110115
stringsLength = totalLength;
111116
}
112-
} else {
113-
yield strings;
114-
strings = '';
115-
stringsLength = 0;
116-
yield arg;
117117
}
118118

119119
strings += DELIMITER;
@@ -133,18 +133,3 @@ export function transformCommandReply(
133133

134134
return command.transformReply(rawReply, preserved);
135135
}
136-
137-
export type LegacyCommandArguments = Array<string | number | Buffer | LegacyCommandArguments>;
138-
139-
export function transformLegacyCommandArguments(args: LegacyCommandArguments, flat: RedisCommandArguments = []): RedisCommandArguments {
140-
for (const arg of args) {
141-
if (Array.isArray(arg)) {
142-
transformLegacyCommandArguments(arg, flat);
143-
continue;
144-
}
145-
146-
flat.push(typeof arg === 'number' ? arg.toString() : arg);
147-
}
148-
149-
return flat;
150-
}

0 commit comments

Comments
 (0)