Skip to content

Commit 97af31b

Browse files
fix(core): use correct formatting for aggregate errors in aws-cdk (#32817)
Closes #32237 ### Reason for this change Sometimes when we print `e.message`, `e` is an `AggegateError` - so the message text is incomplete/not formatted correctly. This PR adds a new util function, `formatErrorMessage` which returns `e.message` if it exists, or a correctly formatted string of errors if `e` is an `AggregateError`. ### Description of changes See `formatErrorMessage` function in the newly created file, `packages/aws-cdk/lib/util/error.ts`. All other changes are grunt work replacing `e.message` with `formateErrorMessage(e)`. This PR only does the finding and replacing in the `aws-cdk` package, TBD whether we need to do the same for the rest of the repo. ### Describe any new or updated permissions being added None ### Description of how you validated changes See unit tests in `packages/aws-cdk/test/api/util/error.test.ts` ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --------- Signed-off-by: Sumu <[email protected]>
1 parent a5bd76e commit 97af31b

21 files changed

+91
-27
lines changed

packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smit
44
import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching';
55
import { debug, warning } from '../../logging';
66
import { AuthenticationError } from '../../toolkit/error';
7+
import { formatErrorMessage } from '../../util/error';
78
import { Mode } from '../plugin/mode';
89
import { PluginHost } from '../plugin/plugin';
910

@@ -48,7 +49,7 @@ export class CredentialPlugins {
4849
available = await source.isAvailable();
4950
} catch (e: any) {
5051
// This shouldn't happen, but let's guard against it anyway
51-
warning(`Uncaught exception in ${source.name}: ${e.message}`);
52+
warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`);
5253
available = false;
5354
}
5455

@@ -62,7 +63,7 @@ export class CredentialPlugins {
6263
canProvide = await source.canProvideCredentials(awsAccountId);
6364
} catch (e: any) {
6465
// This shouldn't happen, but let's guard against it anyway
65-
warning(`Uncaught exception in ${source.name}: ${e.message}`);
66+
warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`);
6667
canProvide = false;
6768
}
6869
if (!canProvide) {

packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { makeCachingProvider } from './provider-caching';
1212
import { SDK } from './sdk';
1313
import { debug, warning } from '../../logging';
1414
import { AuthenticationError } from '../../toolkit/error';
15+
import { formatErrorMessage } from '../../util/error';
1516
import { traceMethods } from '../../util/tracing';
1617
import { Mode } from '../plugin/mode';
1718

@@ -281,7 +282,7 @@ export class SdkProvider {
281282
return undefined;
282283
}
283284

284-
debug(`Unable to determine the default AWS account (${e.name}): ${e.message}`);
285+
debug(`Unable to determine the default AWS account (${e.name}): ${formatErrorMessage(e)}`);
285286
return undefined;
286287
}
287288
});

packages/aws-cdk/lib/api/aws-auth/sdk.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ import { Account } from './sdk-provider';
320320
import { defaultCliUserAgent } from './user-agent';
321321
import { debug } from '../../logging';
322322
import { AuthenticationError } from '../../toolkit/error';
323+
import { formatErrorMessage } from '../../util/error';
323324
import { traceMethods } from '../../util/tracing';
324325

325326
export interface S3ClientOptions {
@@ -903,7 +904,7 @@ export class SDK {
903904

904905
return upload.done();
905906
} catch (e: any) {
906-
throw new AuthenticationError(`Upload failed: ${e.message}`);
907+
throw new AuthenticationError(`Upload failed: ${formatErrorMessage(e)}`);
907908
}
908909
},
909910
};

packages/aws-cdk/lib/api/deploy-stack.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { AssetManifestBuilder } from '../util/asset-manifest-builder';
3333
import { determineAllowCrossAccountAssetPublishing } from './util/checks';
3434
import { publishAssets } from '../util/asset-publishing';
3535
import { StringWithoutPlaceholders } from './util/placeholders';
36+
import { formatErrorMessage } from '../util/error';
3637

3738
export type DeployStackResult =
3839
| SuccessfulDeployStackResult
@@ -388,7 +389,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
388389
}
389390
print(
390391
'Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: %s',
391-
e.message,
392+
formatErrorMessage(e),
392393
);
393394
}
394395

@@ -672,7 +673,7 @@ class FullCloudFormationDeployment {
672673
}
673674
finalState = successStack;
674675
} catch (e: any) {
675-
throw new Error(suffixWithErrors(e.message, monitor?.errors));
676+
throw new Error(suffixWithErrors(formatErrorMessage(e), monitor?.errors));
676677
} finally {
677678
await monitor?.stop();
678679
}
@@ -750,7 +751,7 @@ export async function destroyStack(options: DestroyStackOptions) {
750751
throw new Error(`Failed to destroy ${deployName}: ${destroyedStack.stackStatus}`);
751752
}
752753
} catch (e: any) {
753-
throw new Error(suffixWithErrors(e.message, monitor?.errors));
754+
throw new Error(suffixWithErrors(formatErrorMessage(e), monitor?.errors));
754755
} finally {
755756
if (monitor) {
756757
await monitor.stop();

packages/aws-cdk/lib/api/deployments.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type PublishAssetsOptions,
3939
PublishingAws,
4040
} from '../util/asset-publishing';
41+
import { formatErrorMessage } from '../util/error';
4142

4243
const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23;
4344

@@ -588,7 +589,7 @@ export class Deployments {
588589
stackErrorMessage = errors;
589590
}
590591
} catch (e: any) {
591-
stackErrorMessage = suffixWithErrors(e.message, monitor?.errors);
592+
stackErrorMessage = suffixWithErrors(formatErrorMessage(e), monitor?.errors);
592593
} finally {
593594
await monitor?.stop();
594595
}
@@ -756,7 +757,7 @@ export class Deployments {
756757
try {
757758
await envResources.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter);
758759
} catch (e: any) {
759-
throw new Error(`${stackName}: ${e.message}`);
760+
throw new Error(`${stackName}: ${formatErrorMessage(e)}`);
760761
}
761762
}
762763

packages/aws-cdk/lib/api/environment-access.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/s
55
import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources';
66
import { Mode } from './plugin/mode';
77
import { replaceEnvPlaceholders, StringWithoutPlaceholders } from './util/placeholders';
8+
import { formatErrorMessage } from '../util/error';
89

910
/**
1011
* Access particular AWS resources, based on information from the CX manifest
@@ -130,7 +131,7 @@ export class EnvironmentAccess {
130131
try {
131132
return await this.accessStackForLookup(stack);
132133
} catch (e: any) {
133-
warning(`${e.message}`);
134+
warning(`${formatErrorMessage(e)}`);
134135
}
135136
return this.accessStackForStackOperations(stack, Mode.ForReading);
136137
}

packages/aws-cdk/lib/api/environment-resources.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { SDK } from './aws-auth';
33
import { type EcrRepositoryInfo, ToolkitInfo } from './toolkit-info';
44
import { debug, warning } from '../logging';
55
import { Notices } from '../notices';
6+
import { formatErrorMessage } from '../util/error';
67

78
/**
89
* Registry class for `EnvironmentResources`.
@@ -94,7 +95,7 @@ export class EnvironmentResources {
9495
const bootstrapStack = await this.lookupToolkit();
9596
if (bootstrapStack.found && bootstrapStack.version < BOOTSTRAP_TEMPLATE_VERSION_INTRODUCING_GETPARAMETER) {
9697
warning(
97-
`Could not read SSM parameter ${ssmParameterName}: ${e.message}, falling back to version from ${bootstrapStack}`,
98+
`Could not read SSM parameter ${ssmParameterName}: ${formatErrorMessage(e)}, falling back to version from ${bootstrapStack}`,
9899
);
99100
doValidate(bootstrapStack.version, this.environment);
100101
return;

packages/aws-cdk/lib/api/hotswap-deployments.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { isHotswappableStateMachineChange } from './hotswap/stepfunctions-state-
2929
import { NestedStackTemplates, loadCurrentTemplateWithNestedStacks } from './nested-stack-helpers';
3030
import { Mode } from './plugin/mode';
3131
import { CloudFormationStack } from './util/cloudformation';
32+
import { formatErrorMessage } from '../util/error';
3233

3334
// Must use a require() otherwise esbuild complains about calling a namespace
3435
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -423,7 +424,7 @@ async function applyHotswappableChange(sdk: SDK, hotswapOperation: HotswappableC
423424
await hotswapOperation.apply(sdk);
424425
} catch (e: any) {
425426
if (e.name === 'TimeoutError' || e.name === 'AbortError') {
426-
const result: WaiterResult = JSON.parse(e.message);
427+
const result: WaiterResult = JSON.parse(formatErrorMessage(e));
427428
const error = new Error([
428429
`Resource is not in the expected state due to waiter status: ${result.state}`,
429430
result.reason ? `${result.reason}.` : '',

packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CloudFormationStackArtifact, Environment } from '@aws-cdk/cx-api';
22
import type { StackResourceSummary } from '@aws-sdk/client-cloudformation';
33
import { debug } from '../../logging';
4+
import { formatErrorMessage } from '../../util/error';
45
import type { SDK, SdkProvider } from '../aws-auth';
56
import { EnvironmentAccess } from '../environment-access';
67
import { EvaluateCloudFormationTemplate, LazyListStackResources } from '../evaluate-cloudformation-template';
@@ -44,7 +45,7 @@ export async function findCloudWatchLogGroups(
4445
try {
4546
sdk = (await new EnvironmentAccess(sdkProvider, DEFAULT_TOOLKIT_STACK_NAME).accessStackForLookup(stackArtifact)).sdk;
4647
} catch (e: any) {
47-
debug(`Failed to access SDK environment: ${e.message}`);
48+
debug(`Failed to access SDK environment: ${formatErrorMessage(e)}`);
4849
sdk = (await sdkProvider.forEnvironment(resolvedEnv, Mode.ForReading)).sdk;
4950
}
5051

packages/aws-cdk/lib/api/nested-stack-helpers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as fs from 'fs-extra';
44
import type { SDK } from './aws-auth';
55
import { LazyListStackResources, type ListStackResources } from './evaluate-cloudformation-template';
66
import { CloudFormationStack, type Template } from './util/cloudformation';
7+
import { formatErrorMessage } from '../util/error';
78

89
export interface NestedStackTemplates {
910
readonly physicalName: string | undefined;
@@ -129,7 +130,7 @@ async function getNestedStackArn(
129130
const stackResources = await listStackResources?.listStackResources();
130131
return stackResources?.find((sr) => sr.LogicalResourceId === nestedStackLogicalId)?.PhysicalResourceId;
131132
} catch (e: any) {
132-
if (e.message.startsWith('Stack with id ') && e.message.endsWith(' does not exist')) {
133+
if (formatErrorMessage(e).startsWith('Stack with id ') && formatErrorMessage(e).endsWith(' does not exist')) {
133134
return;
134135
}
135136
throw e;

packages/aws-cdk/lib/api/util/cloudformation.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { makeBodyParameter, TemplateBodyParameter } from './template-body-parame
1515
import { debug } from '../../logging';
1616
import { deserializeStructure } from '../../serialize';
1717
import { AssetManifestBuilder } from '../../util/asset-manifest-builder';
18+
import { formatErrorMessage } from '../../util/error';
1819
import type { ICloudFormationClient, SdkProvider } from '../aws-auth';
1920
import type { Deployments } from '../deployments';
2021

@@ -50,7 +51,7 @@ export class CloudFormationStack {
5051
const response = await cfn.describeStacks({ StackName: stackName });
5152
return new CloudFormationStack(cfn, stackName, response.Stacks && response.Stacks[0], retrieveProcessedTemplate);
5253
} catch (e: any) {
53-
if (e.name === 'ValidationError' && e.message === `Stack with id ${stackName} does not exist`) {
54+
if (e.name === 'ValidationError' && formatErrorMessage(e) === `Stack with id ${stackName} does not exist`) {
5455
return new CloudFormationStack(cfn, stackName, undefined);
5556
}
5657
throw e;

packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { StackEvent } from '@aws-sdk/client-cloudformation';
2+
import { formatErrorMessage } from '../../../util/error';
23
import type { ICloudFormationClient } from '../../aws-auth';
34

45
export interface StackEventPollerProps {
@@ -141,7 +142,7 @@ export class StackEventPoller {
141142

142143
}
143144
} catch (e: any) {
144-
if (!(e.name === 'ValidationError' && e.message === `Stack [${this.props.stackName}] does not exist`)) {
145+
if (!(e.name === 'ValidationError' && formatErrorMessage(e) === `Stack [${this.props.stackName}] does not exist`)) {
145146
throw e;
146147
}
147148
}

packages/aws-cdk/lib/cdk-toolkit.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { deserializeStructure, serializeStructure } from './serialize';
5050
import { Configuration, PROJECT_CONFIG } from './settings';
5151
import { ToolkitError } from './toolkit/error';
5252
import { numberFromBool, partition } from './util';
53+
import { formatErrorMessage } from './util/error';
5354
import { validateSnsTopicArn } from './util/validate-notification-arn';
5455
import { Concurrency, WorkGraph } from './util/work-graph';
5556
import { WorkGraphBuilder } from './util/work-graph-builder';
@@ -201,7 +202,7 @@ export class CdkToolkit {
201202
tryLookupRole: true,
202203
});
203204
} catch (e: any) {
204-
debug(e.message);
205+
debug(formatErrorMessage(e));
205206
if (!quiet) {
206207
stream.write(
207208
`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 {
511512
// It has to be exactly this string because an integration test tests for
512513
// "bold(stackname) failed: ResourceNotReady: <error>"
513514
throw new ToolkitError(
514-
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '),
515+
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), formatErrorMessage(e)].join(' '),
515516
);
516517
} finally {
517518
if (options.cloudWatchLogMonitor) {
@@ -603,7 +604,7 @@ export class CdkToolkit {
603604
const elapsedRollbackTime = new Date().getTime() - startRollbackTime;
604605
print('\n✨ Rollback time: %ss\n', formatTime(elapsedRollbackTime));
605606
} catch (e: any) {
606-
error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), e.message);
607+
error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), formatErrorMessage(e));
607608
throw new ToolkitError('Rollback failed (use --force to orphan failing resources)');
608609
}
609610
}

packages/aws-cdk/lib/context-providers/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ContextProviderPlugin } from '../api/plugin/context-provider-plugin';
1515
import { replaceEnvPlaceholders } from '../api/util/placeholders';
1616
import { debug } from '../logging';
1717
import { Context, TRANSIENT_CONTEXT_KEY } from '../settings';
18+
import { formatErrorMessage } from '../util/error';
1819

1920
export type ContextProviderFactory = ((sdk: SdkProvider) => ContextProviderPlugin);
2021
export type ProviderMap = {[name: string]: ContextProviderFactory};
@@ -72,7 +73,7 @@ export async function provideContextValues(
7273
} catch (e: any) {
7374
// Set a specially formatted provider value which will be interpreted
7475
// as a lookup failure in the toolkit.
75-
value = { [cxapi.PROVIDER_ERROR_KEY]: e.message, [TRANSIENT_CONTEXT_KEY]: true };
76+
value = { [cxapi.PROVIDER_ERROR_KEY]: formatErrorMessage(e), [TRANSIENT_CONTEXT_KEY]: true };
7677
}
7778
context.set(key, value);
7879
debug(`Setting "${key}" context to ${JSON.stringify(value)}`);

packages/aws-cdk/lib/init-hooks.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import { shell } from './os';
33
import { ToolkitError } from './toolkit/error';
4+
import { formatErrorMessage } from './util/error';
45

56
export type SubstitutePlaceholders = (...fileNames: string[]) => Promise<void>;
67

@@ -86,6 +87,6 @@ async function dotnetAddProject(targetDirectory: string, context: HookContext, e
8687
try {
8788
await shell(['dotnet', 'sln', slnPath, 'add', csprojPath]);
8889
} catch (e: any) {
89-
throw new ToolkitError(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${e.message}`);
90+
throw new ToolkitError(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${formatErrorMessage(e)}`);
9091
}
9192
};

packages/aws-cdk/lib/init.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { invokeBuiltinHooks } from './init-hooks';
66
import { error, print, warning } from './logging';
77
import { ToolkitError } from './toolkit/error';
88
import { cdkHomeDir, rootDir } from './util/directories';
9+
import { formatErrorMessage } from './util/error';
910
import { rangeFromSemver } from './util/version-range';
1011

1112
/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
@@ -388,7 +389,7 @@ async function postInstallTypescript(canUseNetwork: boolean, cwd: string) {
388389
try {
389390
await execute(command, ['install'], { cwd });
390391
} catch (e: any) {
391-
warning(`${command} install failed: ` + e.message);
392+
warning(`${command} install failed: ` + formatErrorMessage(e));
392393
}
393394
}
394395

packages/aws-cdk/lib/notices.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ToolkitError } from './toolkit/error';
1313
import { loadTreeFromDir, some } from './tree';
1414
import { flatMap } from './util';
1515
import { cdkCacheDir } from './util/directories';
16+
import { formatErrorMessage } from './util/error';
1617
import { versionNumber } from './version';
1718

1819
const CACHE_FILE_PATH = path.join(cdkCacheDir(), 'notices.json');
@@ -429,19 +430,19 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
429430
debug('Notices refreshed');
430431
resolve(data ?? []);
431432
} catch (e: any) {
432-
reject(new ToolkitError(`Failed to parse notices: ${e.message}`));
433+
reject(new ToolkitError(`Failed to parse notices: ${formatErrorMessage(e)}`));
433434
}
434435
});
435436
res.on('error', e => {
436-
reject(new ToolkitError(`Failed to fetch notices: ${e.message}`));
437+
reject(new ToolkitError(`Failed to fetch notices: ${formatErrorMessage(e)}`));
437438
});
438439
} else {
439440
reject(new ToolkitError(`Failed to fetch notices. Status code: ${res.statusCode}`));
440441
}
441442
});
442443
req.on('error', reject);
443444
} catch (e: any) {
444-
reject(new ToolkitError(`HTTPS 'get' call threw an error: ${e.message}`));
445+
reject(new ToolkitError(`HTTPS 'get' call threw an error: ${formatErrorMessage(e)}`));
445446
}
446447
});
447448
}

packages/aws-cdk/lib/util/archive.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { error } from 'console';
22
import { createWriteStream, promises as fs } from 'fs';
33
import * as path from 'path';
44
import * as glob from 'glob';
5+
import { formatErrorMessage } from './error';
56

67
// eslint-disable-next-line @typescript-eslint/no-require-imports
78
const archiver = require('archiver');
@@ -76,7 +77,7 @@ async function moveIntoPlace(source: string, target: string) {
7677
if (e.code !== 'EPERM' || attempts-- <= 0) {
7778
throw e;
7879
}
79-
error(e.message);
80+
error(formatErrorMessage(e));
8081
await sleep(Math.floor(Math.random() * delay));
8182
delay *= 2;
8283
}

packages/aws-cdk/lib/util/error.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Takes in an error and returns a correctly formatted string of its error message.
3+
* If it is an AggregateError, it will return a string with all the inner errors
4+
* formatted and separated by a newline.
5+
*
6+
* @param error The error to format
7+
* @returns A string with the error message(s) of the error
8+
*/
9+
export function formatErrorMessage(error: any): string {
10+
if (error && Array.isArray(error.errors)) {
11+
const innerMessages = error.errors
12+
.map((innerError: { message: any; toString: () => any }) => (innerError?.message || innerError?.toString()))
13+
.join('\n');
14+
return `AggregateError: ${innerMessages}`;
15+
}
16+
17+
// Fallback for regular Error or other types
18+
return error?.message || error?.toString() || 'Unknown error';
19+
}

0 commit comments

Comments
 (0)