Skip to content

Commit

Permalink
Improve IPC error messages (#1090)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed May 24, 2024
1 parent 18e607c commit e6b012a
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 29 deletions.
4 changes: 2 additions & 2 deletions lib/ipc/ipc-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ const validateAdvancedInput = ipcInput => {
try {
serialize(ipcInput);
} catch (error) {
throw new Error(`The \`ipcInput\` option is not serializable with a structured clone.\n${error.message}`);
throw new Error('The `ipcInput` option is not serializable with a structured clone.', {cause: error});
}
};

const validateJsonInput = ipcInput => {
try {
JSON.stringify(ipcInput);
} catch (error) {
throw new Error(`The \`ipcInput\` option is not serializable with JSON.\n${error.message}`);
throw new Error('The `ipcInput` option is not serializable with JSON.', {cause: error});
}
};

Expand Down
2 changes: 1 addition & 1 deletion lib/ipc/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const handleEpipeError = (error, isSubprocess) => {
// Works with both `serialization: 'advanced'` and `serialization: 'json'`.
export const handleSerializationError = (error, isSubprocess, message) => {
if (isSerializationError(error)) {
error.message = `${getNamespaceName(isSubprocess)}sendMessage()'s argument type is invalid: the message cannot be serialized: ${String(message)}.\n${error.message}`;
throw new Error(`${getNamespaceName(isSubprocess)}sendMessage()'s argument type is invalid: the message cannot be serialized: ${String(message)}.`, {cause: error});
}
};

Expand Down
16 changes: 9 additions & 7 deletions test/ipc/ipc-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@ test('Cannot use the "ipcInput" option with execaSync()', t => {
});

test('Invalid "ipcInput" option v8 format', t => {
const {message} = t.throws(() => {
const {message, cause} = t.throws(() => {
execa('empty.js', {ipcInput() {}});
});
t.is(message, 'The `ipcInput` option is not serializable with a structured clone.\nipcInput() {} could not be cloned.');
t.is(message, 'The `ipcInput` option is not serializable with a structured clone.');
t.is(cause.message, 'ipcInput() {} could not be cloned.');
});

test('Invalid "ipcInput" option JSON format', t => {
const {message} = t.throws(() => {
const {message, cause} = t.throws(() => {
execa('empty.js', {ipcInput: 0n, serialization: 'json'});
});
t.is(message, 'The `ipcInput` option is not serializable with JSON.\nDo not know how to serialize a BigInt');
t.is(message, 'The `ipcInput` option is not serializable with JSON.');
t.is(cause.message, 'Do not know how to serialize a BigInt');
});

test('Handles "ipcInput" option during sending', async t => {
await t.throwsAsync(execa('empty.js', {ipcInput: 0n}), {
message: /The "message" argument must be one of type string/,
});
const {message, cause} = await t.throwsAsync(execa('empty.js', {ipcInput: 0n}));
t.true(message.includes('subprocess.sendMessage()\'s argument type is invalid: the message cannot be serialized: 0.'));
t.true(cause.cause.message.includes('The "message" argument must be one of type string'));
});
8 changes: 5 additions & 3 deletions test/ipc/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ test('Disconnects IPC on exports.sendMessage() error', async t => {
await subprocess.sendMessage(foobarString);
t.is(await subprocess.getOneMessage(), foobarString);

const {message} = await t.throwsAsync(subprocess.sendMessage(0n));
t.true(message.includes('subprocess.sendMessage()\'s argument type is invalid'));
const {message, cause} = await t.throwsAsync(subprocess.sendMessage(0n));
t.is(message, 'subprocess.sendMessage()\'s argument type is invalid: the message cannot be serialized: 0.');
t.true(cause.message.includes('The "message" argument must be one of type string'));

const {exitCode, isTerminated, stderr} = await t.throwsAsync(subprocess);
t.is(exitCode, 1);
Expand All @@ -79,7 +80,8 @@ test('Disconnects IPC on subprocess.sendMessage() error', async t => {
const {exitCode, isTerminated, stderr} = await t.throwsAsync(subprocess);
t.is(exitCode, 1);
t.false(isTerminated);
t.true(stderr.includes('sendMessage()\'s argument type is invalid'));
t.true(stderr.includes('Error: sendMessage()\'s argument type is invalid: the message cannot be serialized: 0.'));
t.true(stderr.includes('The "message" argument must be one of type string'));
});

// EPIPE happens based on timing conditions, so we must repeat it until it happens
Expand Down
41 changes: 25 additions & 16 deletions test/ipc/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,42 @@ test('exports.sendMessage() after disconnection', testPostDisconnectionSubproces
test('exports.getOneMessage() after disconnection', testPostDisconnectionSubprocess, 'getOneMessage');
test('exports.getEachMessage() after disconnection', testPostDisconnectionSubprocess, 'getEachMessage');

const testInvalidPayload = async (t, serialization, message) => {
const INVALID_TYPE_MESSAGE = 'The "message" argument must be one of type string';
const UNDEFINED_MESSAGE = 'The "message" argument must be specified';
const CLONE_MESSAGE = 'could not be cloned';
const CYCLE_MESSAGE = 'Converting circular structure to JSON';
const MAX_CALL_STACK_MESSAGE = 'Maximum call stack size exceeded';

const testInvalidPayload = async (t, serialization, message, expectedMessage) => {
const subprocess = execa('empty.js', {ipc: true, serialization});
await t.throwsAsync(subprocess.sendMessage(message), {message: /type is invalid/});
const error = await t.throwsAsync(subprocess.sendMessage(message));
t.true(error.message.includes('subprocess.sendMessage()\'s argument type is invalid: the message cannot be serialized'));
t.true(error.cause.message.includes(expectedMessage));
await subprocess;
};

const cycleObject = {};
cycleObject.self = cycleObject;
const toJsonCycle = {toJSON: () => ({test: true, toJsonCycle})};

test('subprocess.sendMessage() cannot send undefined', testInvalidPayload, 'advanced', undefined);
test('subprocess.sendMessage() cannot send bigints', testInvalidPayload, 'advanced', 0n);
test('subprocess.sendMessage() cannot send symbols', testInvalidPayload, 'advanced', Symbol('test'));
test('subprocess.sendMessage() cannot send functions', testInvalidPayload, 'advanced', () => {});
test('subprocess.sendMessage() cannot send promises', testInvalidPayload, 'advanced', Promise.resolve());
test('subprocess.sendMessage() cannot send proxies', testInvalidPayload, 'advanced', new Proxy({}, {}));
test('subprocess.sendMessage() cannot send Intl', testInvalidPayload, 'advanced', new Intl.Collator());
test('subprocess.sendMessage() cannot send undefined, JSON', testInvalidPayload, 'json', undefined);
test('subprocess.sendMessage() cannot send bigints, JSON', testInvalidPayload, 'json', 0n);
test('subprocess.sendMessage() cannot send symbols, JSON', testInvalidPayload, 'json', Symbol('test'));
test('subprocess.sendMessage() cannot send functions, JSON', testInvalidPayload, 'json', () => {});
test('subprocess.sendMessage() cannot send cycles, JSON', testInvalidPayload, 'json', cycleObject);
test('subprocess.sendMessage() cannot send cycles in toJSON(), JSON', testInvalidPayload, 'json', toJsonCycle);
test('subprocess.sendMessage() cannot send undefined', testInvalidPayload, 'advanced', undefined, UNDEFINED_MESSAGE);
test('subprocess.sendMessage() cannot send bigints', testInvalidPayload, 'advanced', 0n, INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send symbols', testInvalidPayload, 'advanced', Symbol('test'), INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send functions', testInvalidPayload, 'advanced', () => {}, INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send promises', testInvalidPayload, 'advanced', Promise.resolve(), CLONE_MESSAGE);
test('subprocess.sendMessage() cannot send proxies', testInvalidPayload, 'advanced', new Proxy({}, {}), CLONE_MESSAGE);
test('subprocess.sendMessage() cannot send Intl', testInvalidPayload, 'advanced', new Intl.Collator(), CLONE_MESSAGE);
test('subprocess.sendMessage() cannot send undefined, JSON', testInvalidPayload, 'json', undefined, UNDEFINED_MESSAGE);
test('subprocess.sendMessage() cannot send bigints, JSON', testInvalidPayload, 'json', 0n, INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send symbols, JSON', testInvalidPayload, 'json', Symbol('test'), INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send functions, JSON', testInvalidPayload, 'json', () => {}, INVALID_TYPE_MESSAGE);
test('subprocess.sendMessage() cannot send cycles, JSON', testInvalidPayload, 'json', cycleObject, CYCLE_MESSAGE);
test('subprocess.sendMessage() cannot send cycles in toJSON(), JSON', testInvalidPayload, 'json', toJsonCycle, MAX_CALL_STACK_MESSAGE);

test('exports.sendMessage() validates payload', async t => {
const subprocess = execa('ipc-echo-item.js', {ipc: true});
await subprocess.sendMessage([undefined]);
const {message} = await t.throwsAsync(subprocess);
t.true(message.includes('sendMessage()\'s argument type is invalid'));
t.true(message.includes('Error: sendMessage()\'s argument type is invalid: the message cannot be serialized'));
t.true(message.includes(UNDEFINED_MESSAGE));
});

0 comments on commit e6b012a

Please sign in to comment.