diff --git a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts index 8d7541f42287f..1540cf66e1922 100644 --- a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts +++ b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts @@ -4,6 +4,7 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smit import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching'; import { debug, warning } from '../../logging'; import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; import { Mode } from '../plugin/mode'; import { PluginHost } from '../plugin/plugin'; @@ -48,7 +49,7 @@ export class CredentialPlugins { available = await source.isAvailable(); } catch (e: any) { // This shouldn't happen, but let's guard against it anyway - warning(`Uncaught exception in ${source.name}: ${e.message}`); + warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`); available = false; } @@ -62,7 +63,7 @@ export class CredentialPlugins { canProvide = await source.canProvideCredentials(awsAccountId); } catch (e: any) { // This shouldn't happen, but let's guard against it anyway - warning(`Uncaught exception in ${source.name}: ${e.message}`); + warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`); canProvide = false; } if (!canProvide) { diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 0618cd3503acb..551908e5b3e71 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -12,6 +12,7 @@ import { makeCachingProvider } from './provider-caching'; import { SDK } from './sdk'; import { debug, warning } from '../../logging'; import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; import { traceMethods } from '../../util/tracing'; import { Mode } from '../plugin/mode'; @@ -281,7 +282,7 @@ export class SdkProvider { return undefined; } - debug(`Unable to determine the default AWS account (${e.name}): ${e.message}`); + debug(`Unable to determine the default AWS account (${e.name}): ${formatErrorMessage(e)}`); return undefined; } }); diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 722f946623fad..27c98d4cdbb51 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -320,6 +320,7 @@ import { Account } from './sdk-provider'; import { defaultCliUserAgent } from './user-agent'; import { debug } from '../../logging'; import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; import { traceMethods } from '../../util/tracing'; export interface S3ClientOptions { @@ -903,7 +904,7 @@ export class SDK { return upload.done(); } catch (e: any) { - throw new AuthenticationError(`Upload failed: ${e.message}`); + throw new AuthenticationError(`Upload failed: ${formatErrorMessage(e)}`); } }, }; diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 8ec387466a14e..696340d05010e 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -33,6 +33,7 @@ import { AssetManifestBuilder } from '../util/asset-manifest-builder'; import { determineAllowCrossAccountAssetPublishing } from './util/checks'; import { publishAssets } from '../util/asset-publishing'; import { StringWithoutPlaceholders } from './util/placeholders'; +import { formatErrorMessage } from '../util/error'; export type DeployStackResult = | SuccessfulDeployStackResult @@ -388,7 +389,7 @@ export async function deployStack(options: DeployStackOptions): Promise sr.LogicalResourceId === nestedStackLogicalId)?.PhysicalResourceId; } catch (e: any) { - if (e.message.startsWith('Stack with id ') && e.message.endsWith(' does not exist')) { + if (formatErrorMessage(e).startsWith('Stack with id ') && formatErrorMessage(e).endsWith(' does not exist')) { return; } throw e; diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index dcfe8a62dc6ef..420537c08b805 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -15,6 +15,7 @@ import { makeBodyParameter, TemplateBodyParameter } from './template-body-parame import { debug } from '../../logging'; import { deserializeStructure } from '../../serialize'; import { AssetManifestBuilder } from '../../util/asset-manifest-builder'; +import { formatErrorMessage } from '../../util/error'; import type { ICloudFormationClient, SdkProvider } from '../aws-auth'; import type { Deployments } from '../deployments'; @@ -50,7 +51,7 @@ export class CloudFormationStack { const response = await cfn.describeStacks({ StackName: stackName }); return new CloudFormationStack(cfn, stackName, response.Stacks && response.Stacks[0], retrieveProcessedTemplate); } catch (e: any) { - if (e.name === 'ValidationError' && e.message === `Stack with id ${stackName} does not exist`) { + if (e.name === 'ValidationError' && formatErrorMessage(e) === `Stack with id ${stackName} does not exist`) { return new CloudFormationStack(cfn, stackName, undefined); } throw e; diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index efc66da8ef3b0..ff3018aff3efb 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -1,4 +1,5 @@ import type { StackEvent } from '@aws-sdk/client-cloudformation'; +import { formatErrorMessage } from '../../../util/error'; import type { ICloudFormationClient } from '../../aws-auth'; export interface StackEventPollerProps { @@ -141,7 +142,7 @@ export class StackEventPoller { } } catch (e: any) { - if (!(e.name === 'ValidationError' && e.message === `Stack [${this.props.stackName}] does not exist`)) { + if (!(e.name === 'ValidationError' && formatErrorMessage(e) === `Stack [${this.props.stackName}] does not exist`)) { throw e; } } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 32f6616e5cdb6..319380f69b575 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -50,6 +50,7 @@ import { deserializeStructure, serializeStructure } from './serialize'; import { Configuration, PROJECT_CONFIG } from './settings'; import { ToolkitError } from './toolkit/error'; import { numberFromBool, partition } from './util'; +import { formatErrorMessage } from './util/error'; import { validateSnsTopicArn } from './util/validate-notification-arn'; import { Concurrency, WorkGraph } from './util/work-graph'; import { WorkGraphBuilder } from './util/work-graph-builder'; @@ -201,7 +202,7 @@ export class CdkToolkit { tryLookupRole: true, }); } catch (e: any) { - debug(e.message); + debug(formatErrorMessage(e)); if (!quiet) { stream.write( `Checking if the stack ${stack.stackName} exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n`, @@ -511,7 +512,7 @@ export class CdkToolkit { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " throw new ToolkitError( - [`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '), + [`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), formatErrorMessage(e)].join(' '), ); } finally { if (options.cloudWatchLogMonitor) { @@ -603,7 +604,7 @@ export class CdkToolkit { const elapsedRollbackTime = new Date().getTime() - startRollbackTime; print('\n✨ Rollback time: %ss\n', formatTime(elapsedRollbackTime)); } catch (e: any) { - error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), e.message); + error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), formatErrorMessage(e)); throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); } } diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index 7c4eeee8a6789..abc4f9eb708e4 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -15,6 +15,7 @@ import { ContextProviderPlugin } from '../api/plugin/context-provider-plugin'; import { replaceEnvPlaceholders } from '../api/util/placeholders'; import { debug } from '../logging'; import { Context, TRANSIENT_CONTEXT_KEY } from '../settings'; +import { formatErrorMessage } from '../util/error'; export type ContextProviderFactory = ((sdk: SdkProvider) => ContextProviderPlugin); export type ProviderMap = {[name: string]: ContextProviderFactory}; @@ -72,7 +73,7 @@ export async function provideContextValues( } catch (e: any) { // Set a specially formatted provider value which will be interpreted // as a lookup failure in the toolkit. - value = { [cxapi.PROVIDER_ERROR_KEY]: e.message, [TRANSIENT_CONTEXT_KEY]: true }; + value = { [cxapi.PROVIDER_ERROR_KEY]: formatErrorMessage(e), [TRANSIENT_CONTEXT_KEY]: true }; } context.set(key, value); debug(`Setting "${key}" context to ${JSON.stringify(value)}`); diff --git a/packages/aws-cdk/lib/init-hooks.ts b/packages/aws-cdk/lib/init-hooks.ts index c2334e452b32f..b3f1c4ec48024 100644 --- a/packages/aws-cdk/lib/init-hooks.ts +++ b/packages/aws-cdk/lib/init-hooks.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { shell } from './os'; import { ToolkitError } from './toolkit/error'; +import { formatErrorMessage } from './util/error'; export type SubstitutePlaceholders = (...fileNames: string[]) => Promise; @@ -86,6 +87,6 @@ async function dotnetAddProject(targetDirectory: string, context: HookContext, e try { await shell(['dotnet', 'sln', slnPath, 'add', csprojPath]); } catch (e: any) { - throw new ToolkitError(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${e.message}`); + throw new ToolkitError(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${formatErrorMessage(e)}`); } }; diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index d1e61b8173c42..7fe5854fd4704 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -6,6 +6,7 @@ import { invokeBuiltinHooks } from './init-hooks'; import { error, print, warning } from './logging'; import { ToolkitError } from './toolkit/error'; import { cdkHomeDir, rootDir } from './util/directories'; +import { formatErrorMessage } from './util/error'; import { rangeFromSemver } from './util/version-range'; /* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module @@ -388,7 +389,7 @@ async function postInstallTypescript(canUseNetwork: boolean, cwd: string) { try { await execute(command, ['install'], { cwd }); } catch (e: any) { - warning(`${command} install failed: ` + e.message); + warning(`${command} install failed: ` + formatErrorMessage(e)); } } diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index ca0f1487a97fc..5a4c8e3adf89c 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -13,6 +13,7 @@ import { ToolkitError } from './toolkit/error'; import { loadTreeFromDir, some } from './tree'; import { flatMap } from './util'; import { cdkCacheDir } from './util/directories'; +import { formatErrorMessage } from './util/error'; import { versionNumber } from './version'; const CACHE_FILE_PATH = path.join(cdkCacheDir(), 'notices.json'); @@ -429,11 +430,11 @@ export class WebsiteNoticeDataSource implements NoticeDataSource { debug('Notices refreshed'); resolve(data ?? []); } catch (e: any) { - reject(new ToolkitError(`Failed to parse notices: ${e.message}`)); + reject(new ToolkitError(`Failed to parse notices: ${formatErrorMessage(e)}`)); } }); res.on('error', e => { - reject(new ToolkitError(`Failed to fetch notices: ${e.message}`)); + reject(new ToolkitError(`Failed to fetch notices: ${formatErrorMessage(e)}`)); }); } else { reject(new ToolkitError(`Failed to fetch notices. Status code: ${res.statusCode}`)); @@ -441,7 +442,7 @@ export class WebsiteNoticeDataSource implements NoticeDataSource { }); req.on('error', reject); } catch (e: any) { - reject(new ToolkitError(`HTTPS 'get' call threw an error: ${e.message}`)); + reject(new ToolkitError(`HTTPS 'get' call threw an error: ${formatErrorMessage(e)}`)); } }); } diff --git a/packages/aws-cdk/lib/util/archive.ts b/packages/aws-cdk/lib/util/archive.ts index 26e39ad029e96..c5f1b74b0fa49 100644 --- a/packages/aws-cdk/lib/util/archive.ts +++ b/packages/aws-cdk/lib/util/archive.ts @@ -2,6 +2,7 @@ import { error } from 'console'; import { createWriteStream, promises as fs } from 'fs'; import * as path from 'path'; import * as glob from 'glob'; +import { formatErrorMessage } from './error'; // eslint-disable-next-line @typescript-eslint/no-require-imports const archiver = require('archiver'); @@ -76,7 +77,7 @@ async function moveIntoPlace(source: string, target: string) { if (e.code !== 'EPERM' || attempts-- <= 0) { throw e; } - error(e.message); + error(formatErrorMessage(e)); await sleep(Math.floor(Math.random() * delay)); delay *= 2; } diff --git a/packages/aws-cdk/lib/util/error.ts b/packages/aws-cdk/lib/util/error.ts new file mode 100644 index 0000000000000..f858a5b70934e --- /dev/null +++ b/packages/aws-cdk/lib/util/error.ts @@ -0,0 +1,19 @@ +/** + * Takes in an error and returns a correctly formatted string of its error message. + * If it is an AggregateError, it will return a string with all the inner errors + * formatted and separated by a newline. + * + * @param error The error to format + * @returns A string with the error message(s) of the error + */ +export function formatErrorMessage(error: any): string { + if (error && Array.isArray(error.errors)) { + const innerMessages = error.errors + .map((innerError: { message: any; toString: () => any }) => (innerError?.message || innerError?.toString())) + .join('\n'); + return `AggregateError: ${innerMessages}`; + } + + // Fallback for regular Error or other types + return error?.message || error?.toString() || 'Unknown error'; +} diff --git a/packages/aws-cdk/test/api/fake-sts.ts b/packages/aws-cdk/test/api/fake-sts.ts index e6a8a37fd0c77..5e2162981e92c 100644 --- a/packages/aws-cdk/test/api/fake-sts.ts +++ b/packages/aws-cdk/test/api/fake-sts.ts @@ -2,6 +2,7 @@ import { AssumeRoleCommand, GetCallerIdentityCommand, Tag } from '@aws-sdk/clien import * as nock from 'nock'; import * as uuid from 'uuid'; import * as xmlJs from 'xml-js'; +import { formatErrorMessage } from '../../lib/util/error'; import { mockSTSClient } from '../util/mock-sdk'; interface RegisteredIdentity { @@ -81,7 +82,7 @@ export class FakeSts { Error: { Type: 'Sender', Code: e.name ?? 'Error', - Message: e.message, + Message: formatErrorMessage(e), }, RequestId: '1', }, diff --git a/packages/aws-cdk/test/api/util/error.test.ts b/packages/aws-cdk/test/api/util/error.test.ts new file mode 100644 index 0000000000000..2454e74c66196 --- /dev/null +++ b/packages/aws-cdk/test/api/util/error.test.ts @@ -0,0 +1,26 @@ +import { formatErrorMessage } from '../../../lib/util/error'; + +describe('formatErrorMessage', () => { + test('should return the formatted message for a regular Error object', () => { + const error = new Error('Something went wrong'); + const result = formatErrorMessage(error); + expect(result).toBe('Something went wrong'); + }); + + test('should return the formatted message for an AggregateError', () => { + const error = { + errors: [ + new Error('Inner error 1'), + new Error('Inner error 2'), + new Error('Inner error 3'), + ], + }; + const result = formatErrorMessage(error); + expect(result).toBe('AggregateError: Inner error 1\nInner error 2\nInner error 3'); + }); + + test('should return "Unknown error" for null or undefined error', () => { + expect(formatErrorMessage(null)).toBe('Unknown error'); + expect(formatErrorMessage(undefined)).toBe('Unknown error'); + }); +});