sendContentMessageToGroup: Comprehensive error check before failover
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
84dc49f2ea
commit
8d170dcf74
|
@ -3,9 +3,24 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import { _analyzeSenderKeyDevices, _waitForAll } from '../../util/sendToGroup';
|
import {
|
||||||
|
_analyzeSenderKeyDevices,
|
||||||
|
_waitForAll,
|
||||||
|
_shouldFailSend,
|
||||||
|
} from '../../util/sendToGroup';
|
||||||
|
|
||||||
import type { DeviceType } from '../../textsecure/Types.d';
|
import type { DeviceType } from '../../textsecure/Types.d';
|
||||||
|
import {
|
||||||
|
ConnectTimeoutError,
|
||||||
|
HTTPError,
|
||||||
|
MessageError,
|
||||||
|
OutgoingIdentityKeyError,
|
||||||
|
OutgoingMessageError,
|
||||||
|
SendMessageChallengeError,
|
||||||
|
SendMessageNetworkError,
|
||||||
|
SendMessageProtoError,
|
||||||
|
UnregisteredUserError,
|
||||||
|
} from '../../textsecure/Errors';
|
||||||
|
|
||||||
describe('sendToGroup', () => {
|
describe('sendToGroup', () => {
|
||||||
describe('#_analyzeSenderKeyDevices', () => {
|
describe('#_analyzeSenderKeyDevices', () => {
|
||||||
|
@ -135,7 +150,7 @@ describe('sendToGroup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#_waitForAll', () => {
|
describe('#_waitForAll', () => {
|
||||||
it('returns nothing if new and previous lists are the same', async () => {
|
it('returns result of provided tasks', async () => {
|
||||||
const task1 = () => Promise.resolve(1);
|
const task1 = () => Promise.resolve(1);
|
||||||
const task2 = () => Promise.resolve(2);
|
const task2 = () => Promise.resolve(2);
|
||||||
const task3 = () => Promise.resolve(3);
|
const task3 = () => Promise.resolve(3);
|
||||||
|
@ -148,4 +163,158 @@ describe('sendToGroup', () => {
|
||||||
assert.deepEqual(result, [1, 2, 3]);
|
assert.deepEqual(result, [1, 2, 3]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#_shouldFailSend', () => {
|
||||||
|
it('returns false for a generic error', async () => {
|
||||||
|
const error = new Error('generic');
|
||||||
|
assert.isFalse(_shouldFailSend(error, 'testing generic'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for any error with 'untrusted' identity", async () => {
|
||||||
|
const error = new Error('This was an untrusted identity.');
|
||||||
|
assert.isTrue(_shouldFailSend(error, 'logId'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for certain types of error subclasses', async () => {
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new OutgoingIdentityKeyError(
|
||||||
|
'something',
|
||||||
|
new Uint8Array(),
|
||||||
|
200,
|
||||||
|
new Uint8Array()
|
||||||
|
),
|
||||||
|
'testing OutgoingIdentityKeyError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new UnregisteredUserError(
|
||||||
|
'something',
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 400,
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'testing UnregisteredUserError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new ConnectTimeoutError('something'),
|
||||||
|
'testing ConnectTimeoutError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for unspecified error codes', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const error: any = new Error('generic');
|
||||||
|
|
||||||
|
error.code = 422;
|
||||||
|
assert.isFalse(_shouldFailSend(error, 'testing generic 422'));
|
||||||
|
|
||||||
|
error.code = 204;
|
||||||
|
assert.isFalse(_shouldFailSend(error, 'testing generic 204'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for a specified error codes', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const error: any = new Error('generic');
|
||||||
|
error.code = 401;
|
||||||
|
|
||||||
|
assert.isTrue(_shouldFailSend(error, 'testing generic'));
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 404,
|
||||||
|
headers: {},
|
||||||
|
}),
|
||||||
|
'testing HTTPError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new OutgoingMessageError(
|
||||||
|
'something',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 413,
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'testing OutgoingMessageError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new SendMessageNetworkError(
|
||||||
|
'something',
|
||||||
|
null,
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 428,
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'testing SendMessageNetworkError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new SendMessageChallengeError(
|
||||||
|
'something',
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 500,
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'testing SendMessageChallengeError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new MessageError(
|
||||||
|
'something',
|
||||||
|
new HTTPError('something', {
|
||||||
|
code: 508,
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'testing MessageError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('returns true for errors inside of SendMessageProtoError', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new SendMessageProtoError({}),
|
||||||
|
'testing missing errors list'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const error: any = new Error('generic');
|
||||||
|
error.code = 401;
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new SendMessageProtoError({ errors: [error] }),
|
||||||
|
'testing one error with code'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
_shouldFailSend(
|
||||||
|
new SendMessageProtoError({
|
||||||
|
errors: [
|
||||||
|
new Error('something'),
|
||||||
|
new ConnectTimeoutError('something'),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
'testing ConnectTimeoutError'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,12 +23,19 @@ import { Address } from '../types/Address';
|
||||||
import { QualifiedAddress } from '../types/QualifiedAddress';
|
import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||||
import { UUID } from '../types/UUID';
|
import { UUID } from '../types/UUID';
|
||||||
import { isEnabled } from '../RemoteConfig';
|
import { isEnabled } from '../RemoteConfig';
|
||||||
|
import { isRecord } from './isRecord';
|
||||||
|
|
||||||
import { isOlderThan } from './timestamp';
|
import { isOlderThan } from './timestamp';
|
||||||
import type {
|
import type {
|
||||||
GroupSendOptionsType,
|
GroupSendOptionsType,
|
||||||
SendOptionsType,
|
SendOptionsType,
|
||||||
} from '../textsecure/SendMessage';
|
} from '../textsecure/SendMessage';
|
||||||
|
import {
|
||||||
|
ConnectTimeoutError,
|
||||||
|
OutgoingIdentityKeyError,
|
||||||
|
SendMessageProtoError,
|
||||||
|
UnregisteredUserError,
|
||||||
|
} from '../textsecure/Errors';
|
||||||
import type { HTTPError } from '../textsecure/Errors';
|
import type { HTTPError } from '../textsecure/Errors';
|
||||||
import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores';
|
import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
|
@ -188,10 +195,7 @@ export async function sendContentMessageToGroup({
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.name.includes('untrusted identity')) {
|
if (_shouldFailSend(error, logId)) {
|
||||||
log.error(
|
|
||||||
`sendToGroup/${logId}: Failed with 'untrusted identity' error, re-throwing`
|
|
||||||
);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -653,6 +657,86 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
|
|
||||||
// Utility Methods
|
// Utility Methods
|
||||||
|
|
||||||
|
export function _shouldFailSend(error: unknown, logId: string): boolean {
|
||||||
|
const logError = (message: string) => {
|
||||||
|
log.error(`_shouldFailSend/${logId}: ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message.includes('untrusted identity')) {
|
||||||
|
logError("'untrusted identity' error, failing.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof OutgoingIdentityKeyError) {
|
||||||
|
logError('OutgoingIdentityKeyError error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof UnregisteredUserError) {
|
||||||
|
logError('UnregisteredUserError error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ConnectTimeoutError) {
|
||||||
|
logError('ConnectTimeoutError error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known error types captured here:
|
||||||
|
// HTTPError
|
||||||
|
// OutgoingMessageError
|
||||||
|
// SendMessageNetworkError
|
||||||
|
// SendMessageChallengeError
|
||||||
|
// MessageError
|
||||||
|
if (isRecord(error) && typeof error.code === 'number') {
|
||||||
|
if (error.code === 401) {
|
||||||
|
logError('Permissions error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 404) {
|
||||||
|
logError('Missing user or endpoint error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 413) {
|
||||||
|
logError('Rate limit error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 428) {
|
||||||
|
logError('Challenge error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 500) {
|
||||||
|
logError('Server error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 508) {
|
||||||
|
logError('Fail job error, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof SendMessageProtoError) {
|
||||||
|
if (!error.errors || !error.errors.length) {
|
||||||
|
logError('SendMessageProtoError had no errors, failing.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const innerError of error.errors) {
|
||||||
|
const shouldFail = _shouldFailSend(innerError, logId);
|
||||||
|
if (shouldFail) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export async function _waitForAll<T>({
|
export async function _waitForAll<T>({
|
||||||
tasks,
|
tasks,
|
||||||
maxConcurrency = MAX_CONCURRENCY,
|
maxConcurrency = MAX_CONCURRENCY,
|
||||||
|
|
Loading…
Reference in New Issue