Repository: sindresorhus/execa (JavaScript)

Issue:
`error.message` is incorrect with the `encoding: 'buffer'` option
The following:

```js
await execa('node', ['--invalid-flag'], { encoding: 'buffer' })
```

Throws:

```js
file:///home/ether/code/execa/lib/error.js:58
		error = new Error(message);
		        ^

Error: Command failed with exit code 9: node --invalid-flag
110,111,100,101,58,32,98,97,100,32,111,112,116,105,111,110,58,32,45,45,105,110,118,97,108,105,100,45,102,108,97,103

    at makeError (file:///home/ether/code/execa/lib/error.js:58:11)
    at handlePromise (file:///home/ether/code/execa/index.js:146:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///home/ether/code/execa/a.js:3:1 {
```

:robot: :robot: :robot: 

The `stdout`/`stderr` are properly not properly serialized from an `Uint8Array` to a `string` when used in the `error.message`.

Fixed at #701.


Discussion:

Patch:
diff --git a/lib/error.js b/lib/error.js
index e507e0882f..b15d453f4a 100644
--- a/lib/error.js
+++ b/lib/error.js
@@ -1,5 +1,6 @@
 import process from 'node:process';
 import {signalsByName} from 'human-signals';
+import {isUint8Array} from './stdio/utils.js';
 
 const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => {
 	if (timedOut) {
@@ -25,6 +26,10 @@ const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription
 	return 'failed';
 };
 
+const serializeMessagePart = messagePart => isUint8Array(messagePart)
+	? new TextDecoder().decode(messagePart)
+	: messagePart;
+
 export const makeError = ({
 	stdio,
 	all,
@@ -49,7 +54,10 @@ export const makeError = ({
 	const execaMessage = `Command ${prefix}: ${command}`;
 	const isError = Object.prototype.toString.call(error) === '[object Error]';
 	const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage;
-	const message = [shortMessage, stdio[2], stdio[1], ...stdio.slice(3)].filter(Boolean).join('\n');
+	const message = [shortMessage, stdio[2], stdio[1], ...stdio.slice(3)]
+		.filter(Boolean)
+		.map(messagePart => serializeMessagePart(messagePart))
+		.join('\n');
 
 	if (isError) {
 		error.originalMessage = error.message;


Test patch:
diff --git a/test/error.js b/test/error.js
index 40f5b01644..82fdb58601 100644
--- a/test/error.js
+++ b/test/error.js
@@ -98,10 +98,13 @@ test('error.message contains the command', async t => {
 	await t.throwsAsync(execa('exit.js', ['2', 'foo', 'bar']), {message: /exit.js 2 foo bar/});
 });
 
-test('error.message contains stdout/stderr/stdio if available', async t => {
-	const {message} = await t.throwsAsync(execa('echo-fail.js', fullStdio));
+const testStdioMessage = async (t, encoding) => {
+	const {message} = await t.throwsAsync(execa('echo-fail.js', {...fullStdio, encoding}));
 	t.true(message.endsWith('stderr\nstdout\nfd3'));
-});
+};
+
+test('error.message contains stdout/stderr/stdio if available', testStdioMessage, 'utf8');
+test('error.message contains stdout/stderr/stdio even with encoding "buffer"', testStdioMessage, 'buffer');
 
 test('error.message does not contain stdout if not available', async t => {
 	const {message} = await t.throwsAsync(execa('echo-fail.js', {stdio: ['pipe', 'ignore', 'pipe', 'pipe']}));