Skip to content

Commit f4680f0

Browse files
authored
fix #2665 - handle errors in multi/pipeline replies (#2666)
* fix #2665 - handle errors in multi/pipeline replies * fix MultiErrorReply replies type * run tests on all versions, remove console.log, fix bug * add errors iterator helper * test `.errors()` as well
1 parent d6d2064 commit f4680f0

File tree

3 files changed

+52
-8
lines changed

3 files changed

+52
-8
lines changed

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
33
import RedisClient, { RedisClientType } from '.';
44
import { RedisClientMultiCommandType } from './multi-command';
55
import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
6-
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
6+
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
77
import { defineScript } from '../lua-script';
88
import { spy } from 'sinon';
99
import { once } from 'events';
@@ -602,6 +602,23 @@ describe('Client', () => {
602602
...GLOBAL.SERVERS.OPEN,
603603
minimumDockerVersion: [6, 2] // CLIENT INFO
604604
});
605+
606+
testUtils.testWithClient('should handle error replies (#2665)', async client => {
607+
await assert.rejects(
608+
client.multi()
609+
.set('key', 'value')
610+
.hGetAll('key')
611+
.exec(),
612+
err => {
613+
assert.ok(err instanceof MultiErrorReply);
614+
assert.equal(err.replies.length, 2);
615+
assert.deepEqual(err.errorIndexes, [1]);
616+
assert.ok(err.replies[1] instanceof ErrorReply);
617+
assert.deepEqual([...err.errors()], [err.replies[1]]);
618+
return true;
619+
}
620+
);
621+
}, GLOBAL.SERVERS.OPEN);
605622
});
606623

607624
testUtils.testWithClient('scripts', async client => {

packages/client/lib/errors.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { RedisCommandRawReply } from './commands';
2+
13
export class AbortError extends Error {
24
constructor() {
35
super('The command was aborted');
@@ -63,3 +65,20 @@ export class ErrorReply extends Error {
6365
this.stack = undefined;
6466
}
6567
}
68+
69+
export class MultiErrorReply extends ErrorReply {
70+
replies;
71+
errorIndexes;
72+
73+
constructor(replies: Array<RedisCommandRawReply | ErrorReply>, errorIndexes: Array<number>) {
74+
super(`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`);
75+
this.replies = replies;
76+
this.errorIndexes = errorIndexes;
77+
}
78+
79+
*errors() {
80+
for (const index of this.errorIndexes) {
81+
yield this.replies[index];
82+
}
83+
}
84+
}

packages/client/lib/multi-command.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fCallArguments } from './commander';
22
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands';
3-
import { WatchError } from './errors';
3+
import { ErrorReply, MultiErrorReply, WatchError } from './errors';
44

55
export interface RedisMultiQueuedCommand {
66
args: RedisCommandArguments;
@@ -69,7 +69,7 @@ export default class RedisMultiCommand {
6969
return transformedArguments;
7070
}
7171

72-
handleExecReplies(rawReplies: Array<RedisCommandRawReply>): Array<RedisCommandRawReply> {
72+
handleExecReplies(rawReplies: Array<RedisCommandRawReply | ErrorReply>): Array<RedisCommandRawReply> {
7373
const execReply = rawReplies[rawReplies.length - 1] as (null | Array<RedisCommandRawReply>);
7474
if (execReply === null) {
7575
throw new WatchError();
@@ -78,10 +78,18 @@ export default class RedisMultiCommand {
7878
return this.transformReplies(execReply);
7979
}
8080

81-
transformReplies(rawReplies: Array<RedisCommandRawReply>): Array<RedisCommandRawReply> {
82-
return rawReplies.map((reply, i) => {
83-
const { transformReply, args } = this.queue[i];
84-
return transformReply ? transformReply(reply, args.preserve) : reply;
85-
});
81+
transformReplies(rawReplies: Array<RedisCommandRawReply | ErrorReply>): Array<RedisCommandRawReply> {
82+
const errorIndexes: Array<number> = [],
83+
replies = rawReplies.map((reply, i) => {
84+
if (reply instanceof ErrorReply) {
85+
errorIndexes.push(i);
86+
return reply;
87+
}
88+
const { transformReply, args } = this.queue[i];
89+
return transformReply ? transformReply(reply, args.preserve) : reply;
90+
});
91+
92+
if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes);
93+
return replies;
8694
}
8795
}

0 commit comments

Comments
 (0)