Skip to content

Commit a928748

Browse files
authored
feat(lambda): throw ValidationError instead of untyped errors (#33033)
### Issue `aws-lambda` for #32569 ### Description of changes Updated thrown errors. ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is basically a refactor of existing code. ### 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*
1 parent 61e876b commit a928748

16 files changed

+105
-91
lines changed

packages/aws-cdk-lib/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [
1515

1616

1717
// no-throw-default-error
18-
const modules = ['aws-s3'];
18+
const modules = ['aws-s3', 'aws-lambda'];
1919
baseConfig.overrides.push({
2020
files: modules.map(m => `./${m}/lib/**`),
2121
rules: { "@cdklabs/no-throw-default-error": ['error'] },

packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IConstruct } from 'constructs';
22
import { Architecture } from './architecture';
33
import { IFunction } from './function-base';
4+
import { ValidationError } from '../../core/lib/errors';
45
import { Stack } from '../../core/lib/stack';
56
import { Token } from '../../core/lib/token';
67
import { RegionInfo } from '../../region-info';
@@ -68,8 +69,8 @@ function getLayerArn(scope: IConstruct, type: string, version: string, architect
6869
if (region !== undefined && !Token.isUnresolved(region)) {
6970
const arn = RegionInfo.get(region).adotLambdaLayerArn(type, version, architecture);
7071
if (arn === undefined) {
71-
throw new Error(
72-
`Could not find the ARN information for the ADOT Lambda Layer of type ${type} and version ${version} in ${region}`,
72+
throw new ValidationError(
73+
`Could not find the ARN information for the ADOT Lambda Layer of type ${type} and version ${version} in ${region}`, scope,
7374
);
7475
}
7576
return arn;

packages/aws-cdk-lib/aws-lambda/lib/alias.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as appscaling from '../../aws-applicationautoscaling';
1010
import * as cloudwatch from '../../aws-cloudwatch';
1111
import * as iam from '../../aws-iam';
1212
import { ArnFormat } from '../../core';
13+
import { ValidationError } from '../../core/lib/errors';
1314

1415
export interface IAlias extends IFunction {
1516
/**
@@ -223,7 +224,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
223224
*/
224225
public addAutoScaling(options: AutoScalingOptions): IScalableFunctionAttribute {
225226
if (this.scalableAlias) {
226-
throw new Error('AutoScaling already enabled for this alias');
227+
throw new ValidationError('AutoScaling already enabled for this alias', this);
227228
}
228229
return this.scalableAlias = new ScalableFunctionAttribute(this, 'AliasScaling', {
229230
minCapacity: options.minCapacity ?? 1,
@@ -262,12 +263,12 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
262263
*/
263264
private validateAdditionalWeights(weights: VersionWeight[]) {
264265
const total = weights.map(w => {
265-
if (w.weight < 0 || w.weight > 1) { throw new Error(`Additional version weight must be between 0 and 1, got: ${w.weight}`); }
266+
if (w.weight < 0 || w.weight > 1) { throw new ValidationError(`Additional version weight must be between 0 and 1, got: ${w.weight}`, this); }
266267
return w.weight;
267268
}).reduce((a, x) => a + x);
268269

269270
if (total > 1) {
270-
throw new Error(`Sum of additional version weights must not exceed 1, got: ${total}`);
271+
throw new ValidationError(`Sum of additional version weights must not exceed 1, got: ${total}`, this);
271272
}
272273
}
273274

@@ -282,7 +283,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
282283
}
283284

284285
if (props.provisionedConcurrentExecutions <= 0) {
285-
throw new Error('provisionedConcurrentExecutions must have value greater than or equal to 1');
286+
throw new ValidationError('provisionedConcurrentExecutions must have value greater than or equal to 1', this);
286287
}
287288

288289
return { provisionedConcurrentExecutions: props.provisionedConcurrentExecutions };

packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Construct } from 'constructs';
22
import { CfnCodeSigningConfig } from './lambda.generated';
33
import { ISigningProfile } from '../../aws-signer';
44
import { ArnFormat, IResource, Resource, Stack } from '../../core';
5+
import { ValidationError } from '../../core/lib/errors';
56

67
/**
78
* Code signing configuration policy for deployment validation failure.
@@ -78,10 +79,10 @@ export class CodeSigningConfig extends Resource implements ICodeSigningConfig {
7879
* @param id The construct's name.
7980
* @param codeSigningConfigArn The ARN of code signing config.
8081
*/
81-
public static fromCodeSigningConfigArn( scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig {
82+
public static fromCodeSigningConfigArn(scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig {
8283
const codeSigningProfileId = Stack.of(scope).splitArn(codeSigningConfigArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
8384
if (!codeSigningProfileId) {
84-
throw new Error(`Code signing config ARN must be in the format 'arn:<partition>:lambda:<region>:<account>:code-signing-config:<codeSigningConfigArn>', got: '${codeSigningConfigArn}'`);
85+
throw new ValidationError(`Code signing config ARN must be in the format 'arn:<partition>:lambda:<region>:<account>:code-signing-config:<codeSigningConfigArn>', got: '${codeSigningConfigArn}'`, scope);
8586
}
8687
const assertedCodeSigningProfileId = codeSigningProfileId;
8788
class Import extends Resource implements ICodeSigningConfig {

packages/aws-cdk-lib/aws-lambda/lib/code.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IKey } from '../../aws-kms';
77
import * as s3 from '../../aws-s3';
88
import * as s3_assets from '../../aws-s3-assets';
99
import * as cdk from '../../core';
10+
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
1011

1112
/**
1213
* Represents the Lambda Handler Code.
@@ -83,7 +84,7 @@ export abstract class Code {
8384
options?: CustomCommandOptions,
8485
): AssetCode {
8586
if (command.length === 0) {
86-
throw new Error('command must contain at least one argument. For example, ["node", "buildFile.js"].');
87+
throw new UnscopedValidationError('command must contain at least one argument. For example, ["node", "buildFile.js"].');
8788
}
8889

8990
const cmd = command[0];
@@ -94,10 +95,10 @@ export abstract class Code {
9495
: spawnSync(cmd, commandArguments, options.commandOptions);
9596

9697
if (proc.error) {
97-
throw new Error(`Failed to execute custom command: ${proc.error}`);
98+
throw new UnscopedValidationError(`Failed to execute custom command: ${proc.error}`);
9899
}
99100
if (proc.status !== 0) {
100-
throw new Error(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`);
101+
throw new UnscopedValidationError(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`);
101102
}
102103

103104
return new AssetCode(output, options);
@@ -275,7 +276,7 @@ export class S3Code extends Code {
275276
super();
276277

277278
if (!bucket.bucketName) {
278-
throw new Error('bucketName is undefined for the provided bucket');
279+
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
279280
}
280281

281282
this.bucketName = bucket.bucketName;
@@ -303,7 +304,7 @@ export class S3CodeV2 extends Code {
303304
constructor(bucket: s3.IBucket, private key: string, private options?: BucketOptions) {
304305
super();
305306
if (!bucket.bucketName) {
306-
throw new Error('bucketName is undefined for the provided bucket');
307+
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
307308
}
308309

309310
this.bucketName = bucket.bucketName;
@@ -332,7 +333,7 @@ export class InlineCode extends Code {
332333
super();
333334

334335
if (code.length === 0) {
335-
throw new Error('Lambda inline code cannot be empty');
336+
throw new UnscopedValidationError('Lambda inline code cannot be empty');
336337
}
337338
}
338339

@@ -366,12 +367,12 @@ export class AssetCode extends Code {
366367
...this.options,
367368
});
368369
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
369-
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
370-
'Create a new Code instance for every stack.');
370+
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
371+
'Create a new Code instance for every stack.', scope);
371372
}
372373

373374
if (!this.asset.isZipArchive) {
374-
throw new Error(`Asset must be a .zip file or a directory (${this.path})`);
375+
throw new ValidationError(`Asset must be a .zip file or a directory (${this.path})`, scope);
375376
}
376377

377378
return {
@@ -385,7 +386,7 @@ export class AssetCode extends Code {
385386

386387
public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
387388
if (!this.asset) {
388-
throw new Error('bindToResource() must be called after bind()');
389+
throw new ValidationError('bindToResource() must be called after bind()', resource);
389390
}
390391

391392
const resourceProperty = options.resourceProperty || 'Code';
@@ -497,15 +498,15 @@ export class CfnParametersCode extends Code {
497498
if (this._bucketNameParam) {
498499
return this._bucketNameParam.logicalId;
499500
} else {
500-
throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property');
501+
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property');
501502
}
502503
}
503504

504505
public get objectKeyParam(): string {
505506
if (this._objectKeyParam) {
506507
return this._objectKeyParam.logicalId;
507508
} else {
508-
throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property');
509+
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property');
509510
}
510511
}
511512
}
@@ -628,8 +629,8 @@ export class AssetImageCode extends Code {
628629
});
629630
this.asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
630631
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
631-
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
632-
'Create a new Code instance for every stack.');
632+
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
633+
'Create a new Code instance for every stack.', scope);
633634
}
634635

635636
return {
@@ -644,7 +645,7 @@ export class AssetImageCode extends Code {
644645

645646
public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
646647
if (!this.asset) {
647-
throw new Error('bindToResource() must be called after bind()');
648+
throw new ValidationError('bindToResource() must be called after bind()', resource);
648649
}
649650

650651
const resourceProperty = options.resourceProperty || 'Code.ImageUri';

packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DestinationType, IDestination } from './destination';
33
import { IFunction } from './function-base';
44
import { CfnEventInvokeConfig } from './lambda.generated';
55
import { Duration, Resource } from '../../core';
6+
import { ValidationError } from '../../core/lib/errors';
67

78
/**
89
* Options to add an EventInvokeConfig to a function.
@@ -74,11 +75,11 @@ export class EventInvokeConfig extends Resource {
7475
super(scope, id);
7576

7677
if (props.maxEventAge && (props.maxEventAge.toSeconds() < 60 || props.maxEventAge.toSeconds() > 21600)) {
77-
throw new Error('`maximumEventAge` must represent a `Duration` that is between 60 and 21600 seconds.');
78+
throw new ValidationError('`maximumEventAge` must represent a `Duration` that is between 60 and 21600 seconds.', this);
7879
}
7980

8081
if (props.retryAttempts && (props.retryAttempts < 0 || props.retryAttempts > 2)) {
81-
throw new Error('`retryAttempts` must be between 0 and 2.');
82+
throw new ValidationError('`retryAttempts` must be between 0 and 2.', this);
8283
}
8384

8485
new CfnEventInvokeConfig(this, 'Resource', {

packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CfnEventSourceMapping } from './lambda.generated';
55
import * as iam from '../../aws-iam';
66
import { IKey } from '../../aws-kms';
77
import * as cdk from '../../core';
8+
import { ValidationError } from '../../core/lib/errors';
89

910
/**
1011
* The type of authentication protocol or the VPC components for your event source's SourceAccessConfiguration
@@ -402,78 +403,78 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp
402403
super(scope, id);
403404

404405
if (props.eventSourceArn == undefined && props.kafkaBootstrapServers == undefined) {
405-
throw new Error('Either eventSourceArn or kafkaBootstrapServers must be set');
406+
throw new ValidationError('Either eventSourceArn or kafkaBootstrapServers must be set', this);
406407
}
407408

408409
if (props.eventSourceArn !== undefined && props.kafkaBootstrapServers !== undefined) {
409-
throw new Error('eventSourceArn and kafkaBootstrapServers are mutually exclusive');
410+
throw new ValidationError('eventSourceArn and kafkaBootstrapServers are mutually exclusive', this);
410411
}
411412

412413
if (props.provisionedPollerConfig) {
413414
const { minimumPollers, maximumPollers } = props.provisionedPollerConfig;
414415
if (minimumPollers != undefined) {
415416
if (minimumPollers < 1 || minimumPollers > 200) {
416-
throw new Error('Minimum provisioned pollers must be between 1 and 200 inclusive');
417+
throw new ValidationError('Minimum provisioned pollers must be between 1 and 200 inclusive', this);
417418
}
418419
}
419420
if (maximumPollers != undefined) {
420421
if (maximumPollers < 1 || maximumPollers > 2000) {
421-
throw new Error('Maximum provisioned pollers must be between 1 and 2000 inclusive');
422+
throw new ValidationError('Maximum provisioned pollers must be between 1 and 2000 inclusive', this);
422423
}
423424
}
424425
if (minimumPollers != undefined && maximumPollers != undefined) {
425426
if (minimumPollers > maximumPollers) {
426-
throw new Error('Minimum provisioned pollers must be less than or equal to maximum provisioned pollers');
427+
throw new ValidationError('Minimum provisioned pollers must be less than or equal to maximum provisioned pollers', this);
427428
}
428429
}
429430
}
430431

431432
if (props.kafkaBootstrapServers && (props.kafkaBootstrapServers?.length < 1)) {
432-
throw new Error('kafkaBootStrapServers must not be empty if set');
433+
throw new ValidationError('kafkaBootStrapServers must not be empty if set', this);
433434
}
434435

435436
if (props.maxBatchingWindow && props.maxBatchingWindow.toSeconds() > 300) {
436-
throw new Error(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`);
437+
throw new ValidationError(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`, this);
437438
}
438439

439440
if (props.maxConcurrency && !cdk.Token.isUnresolved(props.maxConcurrency) && (props.maxConcurrency < 2 || props.maxConcurrency > 1000)) {
440-
throw new Error('maxConcurrency must be between 2 and 1000 concurrent instances');
441+
throw new ValidationError('maxConcurrency must be between 2 and 1000 concurrent instances', this);
441442
}
442443

443444
if (props.maxRecordAge && (props.maxRecordAge.toSeconds() < 60 || props.maxRecordAge.toDays({ integral: false }) > 7)) {
444-
throw new Error('maxRecordAge must be between 60 seconds and 7 days inclusive');
445+
throw new ValidationError('maxRecordAge must be between 60 seconds and 7 days inclusive', this);
445446
}
446447

447448
props.retryAttempts !== undefined && cdk.withResolved(props.retryAttempts, (attempts) => {
448449
if (attempts < 0 || attempts > 10000) {
449-
throw new Error(`retryAttempts must be between 0 and 10000 inclusive, got ${attempts}`);
450+
throw new ValidationError(`retryAttempts must be between 0 and 10000 inclusive, got ${attempts}`, this);
450451
}
451452
});
452453

453454
props.parallelizationFactor !== undefined && cdk.withResolved(props.parallelizationFactor, (factor) => {
454455
if (factor < 1 || factor > 10) {
455-
throw new Error(`parallelizationFactor must be between 1 and 10 inclusive, got ${factor}`);
456+
throw new ValidationError(`parallelizationFactor must be between 1 and 10 inclusive, got ${factor}`, this);
456457
}
457458
});
458459

459460
if (props.tumblingWindow && !cdk.Token.isUnresolved(props.tumblingWindow) && props.tumblingWindow.toSeconds() > 900) {
460-
throw new Error(`tumblingWindow cannot be over 900 seconds, got ${props.tumblingWindow.toSeconds()}`);
461+
throw new ValidationError(`tumblingWindow cannot be over 900 seconds, got ${props.tumblingWindow.toSeconds()}`, this);
461462
}
462463

463464
if (props.startingPosition === StartingPosition.AT_TIMESTAMP && !props.startingPositionTimestamp) {
464-
throw new Error('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP');
465+
throw new ValidationError('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP', this);
465466
}
466467

467468
if (props.startingPosition !== StartingPosition.AT_TIMESTAMP && props.startingPositionTimestamp) {
468-
throw new Error('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP');
469+
throw new ValidationError('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP', this);
469470
}
470471

471472
if (props.kafkaConsumerGroupId) {
472473
this.validateKafkaConsumerGroupIdOrThrow(props.kafkaConsumerGroupId);
473474
}
474475

475476
if (props.filterEncryption !== undefined && props.filters == undefined) {
476-
throw new Error('filter criteria must be provided to enable setting filter criteria encryption');
477+
throw new ValidationError('filter criteria must be provided to enable setting filter criteria encryption', this);
477478
}
478479

479480
/**
@@ -540,13 +541,13 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp
540541
}
541542

542543
if (kafkaConsumerGroupId.length > 200 || kafkaConsumerGroupId.length < 1) {
543-
throw new Error('kafkaConsumerGroupId must be a valid string between 1 and 200 characters');
544+
throw new ValidationError('kafkaConsumerGroupId must be a valid string between 1 and 200 characters', this);
544545
}
545546

546547
const regex = new RegExp(/[a-zA-Z0-9-\/*:_+=.@-]*/);
547548
const patternMatch = regex.exec(kafkaConsumerGroupId);
548549
if (patternMatch === null || patternMatch[0] !== kafkaConsumerGroupId) {
549-
throw new Error('kafkaConsumerGroupId contains invalid characters. Allowed values are "[a-zA-Z0-9-\/*:_+=.@-]"');
550+
throw new ValidationError('kafkaConsumerGroupId contains invalid characters. Allowed values are "[a-zA-Z0-9-\/*:_+=.@-]"', this);
550551
}
551552
}
552553
}

0 commit comments

Comments
 (0)