Skip to content

W-18218754: add logic to handle multiple errors returned from api call #677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/SfCommandError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { inspect } from 'node:util';
import { SfError, StructuredMessage } from '@salesforce/core';
import { AnyJson } from '@salesforce/ts-types';
import { computeErrorCode } from './errorHandling.js';
import { computeErrorCode, computeErrorData } from './errorHandling.js';

// These types are 90% the same as SfErrorOptions (but they aren't exported to extend)
type ErrorDataProperties = AnyJson;
Expand Down Expand Up @@ -75,7 +75,7 @@ export class SfCommandError extends SfError {
code: 'code' in err && err.code ? err.code : exitCode.toString(10),
cause: sfError.cause,
commandName: 'commandName' in err ? err.commandName : commandName,
data: 'data' in err ? err.data : undefined,
data: computeErrorData(err),
result: 'result' in err ? err.result : undefined,
context: 'context' in err ? err.context : commandName,
warnings,
Expand Down
24 changes: 24 additions & 0 deletions src/errorFormatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Ansis } from 'ansis';
import { Mode, Messages, envVars } from '@salesforce/core';
import { StandardColors } from './ux/standardColors.js';
import { SfCommandError } from './SfCommandError.js';
import { Ux } from './ux/ux.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages');
Expand Down Expand Up @@ -44,6 +45,7 @@ export const formatActions = (
export const formatError = (error: SfCommandError): string =>
[
`${formatErrorPrefix(error)} ${error.message}`,
...formatMultipleErrorMessages(error),
...formatActions(error.actions ?? []),
error.stack && envVars.getString('SF_ENV') === Mode.DEVELOPMENT
? StandardColors.info(`\n*** Internal Diagnostic ***\n\n${inspect(error)}\n******\n`)
Expand All @@ -55,3 +57,25 @@ const formatErrorPrefix = (error: SfCommandError): string =>

const formatErrorCode = (error: SfCommandError): string =>
typeof error.code === 'string' || typeof error.code === 'number' ? ` (${error.code})` : '';

const formatMultipleErrorMessages = (error: SfCommandError): string[] => {
if (!error.data || !Array.isArray(error.data) || error.data.length === 0) {
return [];
}

const errorData = error.data.map((d) => ({
errorCode: (d as { errorCode: string }).errorCode || '',
message: (d as { message: string }).message || '',
}));

const ux = new Ux();
return [
ux.makeTable({
data: errorData,
columns: [
{ key: 'errorCode', name: 'Error Code' },
{ key: 'message', name: 'Message' },
],
}),
];
};
22 changes: 22 additions & 0 deletions src/errorHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { SfError } from '@salesforce/core/sfError';
import { Errors } from '@oclif/core';
import { AnyJson } from '@salesforce/ts-types';

/**
*
Expand Down Expand Up @@ -44,6 +45,27 @@ export const computeErrorCode = (e: Error | SfError | Errors.CLIError): number =
return typeof process.exitCode === 'number' ? process.exitCode : 1;
};

/**
* Computes and extracts error data from different error types.
*
* 1. If the error has a 'data' property with a value, returns that data
* 2. If the error has a 'cause' property with a value:
* - If the cause has a 'data' property, returns cause.data
* - If not, returns undefined
* 3. If none of the above conditions are met, returns undefined
*
* @param e - The error object to extract data from. Can be a standard Error, SfError, or CLIError
* @returns The extracted data as AnyJson or undefined if no data is found
*/
export const computeErrorData = (e: Error | SfError | Errors.CLIError): AnyJson | undefined =>
'data' in e && e.data
? e.data
: 'cause' in e && e.cause
? 'data' in (e.cause as { data: AnyJson | undefined })
? (e.cause as { data: AnyJson | undefined }).data
: undefined
: undefined;

/** identifies gacks via regex. Searches the error message, stack, and recursively checks the cause chain */
export const errorIsGack = (error: Error | SfError): boolean => {
/** see test for samples */
Expand Down
19 changes: 19 additions & 0 deletions test/unit/errorFormatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,23 @@ describe('errorFormatting.formatError()', () => {
expect(errorOutput).to.contain('warnings: undefined');
expect(errorOutput).to.contain('result: undefined');
});

it('should have correct output for multiple errors in table format ', () => {
const sfError = SfError.create({
name: 'myError',
message: 'foo',
actions: ['bar'],
context: 'myContext',
exitCode: 8,
data: [
{ errorCode: 'ERROR_1', message: 'error 1' },
{ errorCode: 'ERROR_2', message: 'error 2' },
],
});
const err = SfCommandError.from(sfError, 'thecommand');
const errorOutput = formatError(err);
expect(errorOutput).to.match(/Error Code.+Message/);
expect(errorOutput).to.match(/ERROR_1.+error 1/);
expect(errorOutput).to.match(/ERROR_2.+error 2/);
});
});
53 changes: 52 additions & 1 deletion test/unit/errorHandling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/
import { expect } from 'chai';
import { SfError } from '@salesforce/core/sfError';
import { computeErrorCode, errorIsGack, errorIsTypeError } from '../../src/errorHandling.js';
import { AnyJson } from '@salesforce/ts-types';
import { Errors } from '@oclif/core';
import { computeErrorCode, computeErrorData, errorIsGack, errorIsTypeError } from '../../src/errorHandling.js';
import { SfCommandError } from '../../src/SfCommandError.js';

describe('typeErrors', () => {
Expand Down Expand Up @@ -197,3 +199,52 @@ describe('SfCommandError.toJson()', () => {
});
});
});

describe('computeErrorData', () => {
interface ErrorWithData extends Error {
data?: AnyJson;
}

it('should return data from error.data when present', () => {
const sfError = SfError.create({
name: 'myError',
message: 'foo',
actions: ['bar'],
context: 'myContext',
exitCode: 8,
data: { foo: 'bar' },
});
expect(computeErrorData(sfError)).to.deep.equal({ foo: 'bar' });
});

it('should return cause.data when error.data is not present but cause.data is', () => {
const sfError = SfError.create({
name: 'myError',
message: 'foo',
actions: ['bar'],
context: 'myContext',
exitCode: 8,
});
const err: ErrorWithData = { name: 'testError', message: 'baz', data: { foo: 'baz' } };
sfError.cause = err;
expect(computeErrorData(sfError)).to.deep.equal({ foo: 'baz' });
});

it('should return undefined when no data or cause is present', () => {
const error = new Error('test error') as ErrorWithData;
expect(computeErrorData(error)).to.be.undefined;
});

it('should handle SfError with data', () => {
const error = new SfError('test error', 'TestError', [], 1, undefined);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(error as any).data = { foo: 'bar' };
expect(computeErrorData(error)).to.deep.equal({ foo: 'bar' });
});

it('should handle CLIError with data', () => {
const err = new Errors.CLIError('Nonexistent flag: --INVALID\nSee more help with --help') as ErrorWithData;
err.data = { foo: 'bar' };
expect(computeErrorData(err)).to.deep.equal({ foo: 'bar' });
});
});
Loading