From e6b012afb1f22c9853d2b25745ca653b47562e98 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 24 May 2024 11:22:02 +0100 Subject: [PATCH] Improve IPC error messages (#1090) --- lib/ipc/ipc-input.js | 4 ++-- lib/ipc/validation.js | 2 +- test/ipc/ipc-input.js | 16 +++++++++------- test/ipc/send.js | 8 +++++--- test/ipc/validation.js | 41 +++++++++++++++++++++++++---------------- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/ipc/ipc-input.js b/lib/ipc/ipc-input.js index df7652766f..908f2ace1c 100644 --- a/lib/ipc/ipc-input.js +++ b/lib/ipc/ipc-input.js @@ -17,7 +17,7 @@ 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}); } }; @@ -25,7 +25,7 @@ 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}); } }; diff --git a/lib/ipc/validation.js b/lib/ipc/validation.js index 5d02de6e4c..65315304a0 100644 --- a/lib/ipc/validation.js +++ b/lib/ipc/validation.js @@ -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}); } }; diff --git a/test/ipc/ipc-input.js b/test/ipc/ipc-input.js index 8f7091c702..0e30d2653f 100644 --- a/test/ipc/ipc-input.js +++ b/test/ipc/ipc-input.js @@ -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')); }); diff --git a/test/ipc/send.js b/test/ipc/send.js index 6c45cf45d8..32d170c73e 100644 --- a/test/ipc/send.js +++ b/test/ipc/send.js @@ -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); @@ -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 diff --git a/test/ipc/validation.js b/test/ipc/validation.js index 70d82a6f02..b7e4d72a52 100644 --- a/test/ipc/validation.js +++ b/test/ipc/validation.js @@ -59,9 +59,17 @@ 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; }; @@ -69,23 +77,24 @@ 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)); });