diff --git a/.changeset/toolkit-lib-metadata-null-guard.md b/.changeset/toolkit-lib-metadata-null-guard.md new file mode 100644 index 0000000000..ce1f45cef0 --- /dev/null +++ b/.changeset/toolkit-lib-metadata-null-guard.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-deployer': patch +--- + +Map the `@aws-cdk/toolkit-lib` `countAssemblyResults` crash (`TypeError: Cannot convert undefined or null to object` when a synthesized stack has no metadata) to a fault instead of a misleading backend `SyntaxError`. diff --git a/packages/backend-deployer/src/cdk_error_mapper.test.ts b/packages/backend-deployer/src/cdk_error_mapper.test.ts index 165c07b0ff..eaf1c71ac8 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.test.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.test.ts @@ -545,3 +545,49 @@ void describe('Node version error per deployment type', () => { ); }); }); + +void describe('toolkit-lib countAssemblyResults metadata crash', () => { + const cdkErrorMapper = new CdkErrorMapper(formatterStub); + + // The TypeError @aws-cdk/toolkit-lib throws when a stack has no metadata. + // Refs aws-amplify/amplify-backend#3316. + const buildCountAssemblyResultsError = () => { + const error = new TypeError('Cannot convert undefined or null to object'); + error.stack = [ + 'TypeError: Cannot convert undefined or null to object', + ' at Object.values ()', + ' at node_modules/@aws-cdk/toolkit-lib/lib/toolkit/private/count-assembly-results.js:12:30', + ' at Array.flatMap ()', + ' at countAssemblyResults (node_modules/@aws-cdk/toolkit-lib/lib/toolkit/private/count-assembly-results.js:12:10)', + ' at synthAndMeasure (node_modules/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.js:1709:59)', + ].join('\n'); + return error; + }; + + void it('classifies the crash as a fault, not a user error', () => { + const error = cdkErrorMapper.getAmplifyError( + buildCountAssemblyResultsError(), + ); + assert.equal(error.name, 'CDKSynthAssemblyMetadataFault'); + // Must be classified as a FAULT (internal/library bug), not a user ERROR + // that would tell the customer to fix their own backend code. + assert.equal(error.classification, 'FAULT'); + assert.match(error.message, /@aws-cdk\/toolkit-lib/); + }); + + void it('preserves the original TypeError as the cause', () => { + const original = buildCountAssemblyResultsError(); + const error = cdkErrorMapper.getAmplifyError(original); + assert.equal(error.cause, original); + }); + + void it('does not intercept unrelated TypeErrors (still a backend SyntaxError)', () => { + const unrelated = new TypeError('foo is not a function'); + unrelated.stack = + 'TypeError: foo is not a function\n at amplify/backend.ts:3:1'; + assert.throws( + () => cdkErrorMapper.getAmplifyError(unrelated), + (thrown: Error) => thrown.name === 'SyntaxError', + ); + }); +}); diff --git a/packages/backend-deployer/src/cdk_error_mapper.ts b/packages/backend-deployer/src/cdk_error_mapper.ts index 41cec7f2cb..c5caf79981 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.ts @@ -167,6 +167,34 @@ export class CdkErrorMapper { }, error, ); + } else if ( + error.name === 'TypeError' && + /Cannot convert undefined or null to object/.test(error.message) && + /countAssemblyResults/.test(error.stack ?? '') + ) { + // @aws-cdk/toolkit-lib bug: countAssemblyResults calls + // Object.values(stack.metadata) with no null guard and throws when a + // synthesized stack has no metadata. This is a library fault, not a + // customer backend error, so it must not fall through to the generic + // TypeError branch below. Refs aws-amplify/amplify-backend#3316. + // This match relies on the TypeError reaching the mapper without being + // wrapped by an intermediate layer: the countAssemblyResults stack frame + // is the stable anchor, while the error.name check would silently miss + // if a layer wraps the error into a new type before it gets here. + // TODO(aws-amplify/amplify-backend#3316): the upstream null guard is + // merged (aws/aws-cdk-cli#1952). Once a @aws-cdk/toolkit-lib release ships + // it and this package bumps its pin, countAssemblyResults no longer throws + // and this branch becomes dead code — remove it as part of that bump. + return new AmplifyFault( + 'CDKSynthAssemblyMetadataFault', + { + message: + 'Unable to synthesize the Amplify backend due to a bug in the CDK toolkit library (@aws-cdk/toolkit-lib) that fails when a synthesized stack has no metadata.', + resolution: + 'This is a known upstream bug in @aws-cdk/toolkit-lib, not an issue in your backend code; retrying is unlikely to help. Track aws-amplify/amplify-backend#3316 for a fix.', + }, + error, + ); } else if ( ['TypeError', 'TransformError', 'ReferenceError', 'SyntaxError'].some( (errorName) => errorName === error.name, @@ -628,6 +656,7 @@ export type CDKDeploymentError = | 'CDKAssetBundleError' | 'CDKNotFoundError' | 'CDKResolveAWSAccountError' + | 'CDKSynthAssemblyMetadataFault' | 'CDKVersionMismatchError' | 'CFNUpdateNotSupportedError' | 'CloudformationResourceCircularDependencyError'