sendContentMessageToGroup: Comprehensive error check before failover

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal 2022-01-31 17:33:01 -08:00 committed by GitHub
parent 84dc49f2ea
commit 8d170dcf74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 259 additions and 6 deletions

View File

@ -3,9 +3,24 @@
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 {
ConnectTimeoutError,
HTTPError,
MessageError,
OutgoingIdentityKeyError,
OutgoingMessageError,
SendMessageChallengeError,
SendMessageNetworkError,
SendMessageProtoError,
UnregisteredUserError,
} from '../../textsecure/Errors';
describe('sendToGroup', () => {
describe('#_analyzeSenderKeyDevices', () => {
@ -135,7 +150,7 @@ describe('sendToGroup', () => {
});
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 task2 = () => Promise.resolve(2);
const task3 = () => Promise.resolve(3);
@ -148,4 +163,158 @@ describe('sendToGroup', () => {
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'
)
);
});
});
});

View File

@ -23,12 +23,19 @@ import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { isEnabled } from '../RemoteConfig';
import { isRecord } from './isRecord';
import { isOlderThan } from './timestamp';
import type {
GroupSendOptionsType,
SendOptionsType,
} from '../textsecure/SendMessage';
import {
ConnectTimeoutError,
OutgoingIdentityKeyError,
SendMessageProtoError,
UnregisteredUserError,
} from '../textsecure/Errors';
import type { HTTPError } from '../textsecure/Errors';
import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores';
import type { ConversationModel } from '../models/conversations';
@ -188,10 +195,7 @@ export async function sendContentMessageToGroup({
throw error;
}
if (error.name.includes('untrusted identity')) {
log.error(
`sendToGroup/${logId}: Failed with 'untrusted identity' error, re-throwing`
);
if (_shouldFailSend(error, logId)) {
throw error;
}
@ -653,6 +657,86 @@ export async function sendToGroupViaSenderKey(options: {
// 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>({
tasks,
maxConcurrency = MAX_CONCURRENCY,