Skip to content

Commit c08c7f0

Browse files
authored
feat(cloudfront): throw ValidationErrors instead of untyped Errors (#33438)
### Issue Relates to #32569 ### Description of changes `ValidationErrors` everywhere ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is 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 9a6e5cc commit c08c7f0

18 files changed

+99
-92
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const enableNoThrowDefaultErrorIn = [
3333
'aws-backup',
3434
'aws-batch',
3535
'aws-cognito',
36+
'aws-cloudfront',
37+
'aws-cloudfront-origins',
3638
'aws-elasticloadbalancing',
3739
'aws-elasticloadbalancingv2',
3840
'aws-elasticloadbalancingv2-actions',

packages/aws-cdk-lib/aws-cloudfront-origins/lib/function-url-origin.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class FunctionUrlOriginWithOAC extends cloudfront.OriginBase {
115115
if (!this.originAccessControl) {
116116
this.originAccessControl = new cloudfront.FunctionUrlOriginAccessControl(scope, 'FunctionUrlOriginAccessControl');
117117
}
118-
this.validateAuthType();
118+
this.validateAuthType(scope);
119119

120120
this.addInvokePermission(scope, options);
121121

@@ -142,7 +142,7 @@ class FunctionUrlOriginWithOAC extends cloudfront.OriginBase {
142142
/**
143143
* Validation method: Ensures that when the OAC signing method is SIGV4_ALWAYS, the authType is set to AWS_IAM.
144144
*/
145-
private validateAuthType() {
145+
private validateAuthType(scope: Construct) {
146146
const cfnOriginAccessControl = this.originAccessControl?.node.children.find(
147147
(child) => child instanceof cloudfront.CfnOriginAccessControl,
148148
) as cloudfront.CfnOriginAccessControl;
@@ -156,7 +156,7 @@ class FunctionUrlOriginWithOAC extends cloudfront.OriginBase {
156156
const isAuthTypeIsNone: boolean = this.functionUrl.authType !== lambda.FunctionUrlAuthType.AWS_IAM;
157157

158158
if (isAlwaysSigning && isAuthTypeIsNone) {
159-
throw new Error('The authType of the Function URL must be set to AWS_IAM when origin access control signing method is SIGV4_ALWAYS.');
159+
throw new cdk.ValidationError('The authType of the Function URL must be set to AWS_IAM when origin access control signing method is SIGV4_ALWAYS.', scope);
160160
}
161161
}
162162
}

packages/aws-cdk-lib/aws-cloudfront-origins/lib/origin-group.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Construct } from 'constructs';
22
import * as cloudfront from '../../aws-cloudfront';
3+
import { ValidationError } from '../../core';
34

45
/** Construction properties for `OriginGroup`. */
56
export interface OriginGroupProps {
@@ -44,7 +45,7 @@ export class OriginGroup implements cloudfront.IOrigin {
4445
public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
4546
const primaryOriginConfig = this.props.primaryOrigin.bind(scope, options);
4647
if (primaryOriginConfig.failoverConfig) {
47-
throw new Error('An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!');
48+
throw new ValidationError('An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!', scope);
4849
}
4950

5051
return {

packages/aws-cdk-lib/aws-cloudfront-origins/lib/private/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export function validateSecondsInRangeOrUndefined(name: string, min: number, max
77
if (duration === undefined) { return; }
88
const value = duration.toSeconds();
99
if (!Number.isInteger(value) || value < min || value > max) {
10-
throw new Error(`${name}: Must be an int between ${min} and ${max} seconds (inclusive); received ${value}.`);
10+
throw new cdk.UnscopedValidationError(`${name}: Must be an int between ${min} and ${max} seconds (inclusive); received ${value}.`);
1111
}
1212
}

packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-bucket-origin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AccessLevel } from '../../aws-cloudfront';
44
import * as iam from '../../aws-iam';
55
import { IKey } from '../../aws-kms';
66
import { IBucket } from '../../aws-s3';
7-
import { Annotations, Aws, Names, Stack } from '../../core';
7+
import { Annotations, Aws, Names, Stack, UnscopedValidationError } from '../../core';
88

99
interface BucketPolicyAction {
1010
readonly action: string;
@@ -262,7 +262,7 @@ class S3BucketOriginWithOAI extends S3BucketOrigin {
262262

263263
protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined {
264264
if (!this.originAccessIdentity) {
265-
throw new Error('Origin access identity cannot be undefined');
265+
throw new UnscopedValidationError('Origin access identity cannot be undefined');
266266
}
267267
return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` };
268268
}

packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Construct } from 'constructs';
22
import { CfnCachePolicy } from './cloudfront.generated';
3-
import { Duration, Names, Resource, Stack, Token, withResolved } from '../../core';
3+
import { Duration, Names, Resource, Stack, Token, UnscopedValidationError, ValidationError, withResolved } from '../../core';
44
import { addConstructMetadata } from '../../core/lib/metadata-resource';
55

66
/**
@@ -148,15 +148,15 @@ export class CachePolicy extends Resource implements ICachePolicy {
148148
const cachePolicyName = props.cachePolicyName ?? `${Names.uniqueId(this).slice(0, 110)}-${Stack.of(this).region}`;
149149

150150
if (!Token.isUnresolved(cachePolicyName) && !cachePolicyName.match(/^[\w-]+$/i)) {
151-
throw new Error(`'cachePolicyName' can only include '-', '_', and alphanumeric characters, got: '${cachePolicyName}'`);
151+
throw new ValidationError(`'cachePolicyName' can only include '-', '_', and alphanumeric characters, got: '${cachePolicyName}'`, this);
152152
}
153153

154154
if (cachePolicyName.length > 128) {
155-
throw new Error(`'cachePolicyName' cannot be longer than 128 characters, got: '${cachePolicyName.length}'`);
155+
throw new ValidationError(`'cachePolicyName' cannot be longer than 128 characters, got: '${cachePolicyName.length}'`, this);
156156
}
157157

158158
if (props.comment && !Token.isUnresolved(props.comment) && props.comment.length > 128) {
159-
throw new Error(`'comment' cannot be longer than 128 characters, got: ${props.comment.length}`);
159+
throw new ValidationError(`'comment' cannot be longer than 128 characters, got: ${props.comment.length}`, this);
160160
}
161161

162162
const minTtl = (props.minTtl ?? Duration.seconds(0)).toSeconds();
@@ -229,7 +229,7 @@ export class CacheCookieBehavior {
229229
*/
230230
public static allowList(...cookies: string[]) {
231231
if (cookies.length === 0) {
232-
throw new Error('At least one cookie to allow must be provided');
232+
throw new UnscopedValidationError('At least one cookie to allow must be provided');
233233
}
234234
return new CacheCookieBehavior('whitelist', cookies);
235235
}
@@ -240,7 +240,7 @@ export class CacheCookieBehavior {
240240
*/
241241
public static denyList(...cookies: string[]) {
242242
if (cookies.length === 0) {
243-
throw new Error('At least one cookie to deny must be provided');
243+
throw new UnscopedValidationError('At least one cookie to deny must be provided');
244244
}
245245
return new CacheCookieBehavior('allExcept', cookies);
246246
}
@@ -265,7 +265,7 @@ export class CacheHeaderBehavior {
265265
/** Listed headers are included in the cache key and are automatically included in requests that CloudFront sends to the origin. */
266266
public static allowList(...headers: string[]) {
267267
if (headers.length === 0) {
268-
throw new Error('At least one header to allow must be provided');
268+
throw new UnscopedValidationError('At least one header to allow must be provided');
269269
}
270270
return new CacheHeaderBehavior('whitelist', headers);
271271
}
@@ -302,7 +302,7 @@ export class CacheQueryStringBehavior {
302302
*/
303303
public static allowList(...queryStrings: string[]) {
304304
if (queryStrings.length === 0) {
305-
throw new Error('At least one query string to allow must be provided');
305+
throw new UnscopedValidationError('At least one query string to allow must be provided');
306306
}
307307
return new CacheQueryStringBehavior('whitelist', queryStrings);
308308
}
@@ -313,7 +313,7 @@ export class CacheQueryStringBehavior {
313313
*/
314314
public static denyList(...queryStrings: string[]) {
315315
if (queryStrings.length === 0) {
316-
throw new Error('At least one query string to deny must be provided');
316+
throw new UnscopedValidationError('At least one query string to deny must be provided');
317317
}
318318
return new CacheQueryStringBehavior('allExcept', queryStrings);
319319
}

packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts

+18-18
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as cloudwatch from '../../aws-cloudwatch';
1515
import * as iam from '../../aws-iam';
1616
import * as lambda from '../../aws-lambda';
1717
import * as s3 from '../../aws-s3';
18-
import { ArnFormat, IResource, Lazy, Resource, Stack, Token, Duration, Names, FeatureFlags, Annotations } from '../../core';
18+
import { ArnFormat, IResource, Lazy, Resource, Stack, Token, Duration, Names, FeatureFlags, Annotations, ValidationError } from '../../core';
1919
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
2020
import { CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 } from '../../cx-api';
2121

@@ -332,7 +332,7 @@ export class Distribution extends Resource implements IDistribution {
332332
if (props.certificate) {
333333
const certificateRegion = Stack.of(this).splitArn(props.certificate.certificateArn, ArnFormat.SLASH_RESOURCE_NAME).region;
334334
if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') {
335-
throw new Error(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`);
335+
throw new ValidationError(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`, this);
336336
}
337337

338338
if ((props.domainNames ?? []).length === 0) {
@@ -491,7 +491,7 @@ export class Distribution extends Resource implements IDistribution {
491491
@MethodMetadata()
492492
public metricOriginLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
493493
if (this.publishAdditionalMetrics !== true) {
494-
throw new Error('Origin latency metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
494+
throw new ValidationError('Origin latency metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
495495
}
496496
return this.metric('OriginLatency', props);
497497
}
@@ -508,7 +508,7 @@ export class Distribution extends Resource implements IDistribution {
508508
@MethodMetadata()
509509
public metricCacheHitRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
510510
if (this.publishAdditionalMetrics !== true) {
511-
throw new Error('Cache hit rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
511+
throw new ValidationError('Cache hit rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
512512
}
513513
return this.metric('CacheHitRate', props);
514514
}
@@ -523,7 +523,7 @@ export class Distribution extends Resource implements IDistribution {
523523
@MethodMetadata()
524524
public metric401ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
525525
if (this.publishAdditionalMetrics !== true) {
526-
throw new Error('401 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
526+
throw new ValidationError('401 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
527527
}
528528
return this.metric('401ErrorRate', props);
529529
}
@@ -538,7 +538,7 @@ export class Distribution extends Resource implements IDistribution {
538538
@MethodMetadata()
539539
public metric403ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
540540
if (this.publishAdditionalMetrics !== true) {
541-
throw new Error('403 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
541+
throw new ValidationError('403 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
542542
}
543543
return this.metric('403ErrorRate', props);
544544
}
@@ -553,7 +553,7 @@ export class Distribution extends Resource implements IDistribution {
553553
@MethodMetadata()
554554
public metric404ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
555555
if (this.publishAdditionalMetrics !== true) {
556-
throw new Error('404 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
556+
throw new ValidationError('404 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
557557
}
558558
return this.metric('404ErrorRate', props);
559559
}
@@ -568,7 +568,7 @@ export class Distribution extends Resource implements IDistribution {
568568
@MethodMetadata()
569569
public metric502ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
570570
if (this.publishAdditionalMetrics !== true) {
571-
throw new Error('502 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
571+
throw new ValidationError('502 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
572572
}
573573
return this.metric('502ErrorRate', props);
574574
}
@@ -583,7 +583,7 @@ export class Distribution extends Resource implements IDistribution {
583583
@MethodMetadata()
584584
public metric503ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
585585
if (this.publishAdditionalMetrics !== true) {
586-
throw new Error('503 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
586+
throw new ValidationError('503 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
587587
}
588588
return this.metric('503ErrorRate', props);
589589
}
@@ -598,7 +598,7 @@ export class Distribution extends Resource implements IDistribution {
598598
@MethodMetadata()
599599
public metric504ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
600600
if (this.publishAdditionalMetrics !== true) {
601-
throw new Error('504 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
601+
throw new ValidationError('504 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this);
602602
}
603603
return this.metric('504ErrorRate', props);
604604
}
@@ -613,7 +613,7 @@ export class Distribution extends Resource implements IDistribution {
613613
@MethodMetadata()
614614
public addBehavior(pathPattern: string, origin: IOrigin, behaviorOptions: AddBehaviorOptions = {}) {
615615
if (pathPattern === '*') {
616-
throw new Error('Only the default behavior can have a path pattern of \'*\'');
616+
throw new ValidationError('Only the default behavior can have a path pattern of \'*\'', this);
617617
}
618618
const originId = this.addOrigin(origin);
619619
this.additionalBehaviors.push(new CacheBehavior(originId, { pathPattern, ...behaviorOptions }));
@@ -651,7 +651,7 @@ export class Distribution extends Resource implements IDistribution {
651651
@MethodMetadata()
652652
public attachWebAclId(webAclId: string) {
653653
if (this.webAclId) {
654-
throw new Error('A WebACL has already been attached to this distribution');
654+
throw new ValidationError('A WebACL has already been attached to this distribution', this);
655655
}
656656
this.validateWebAclId(webAclId);
657657
this.webAclId = webAclId;
@@ -661,7 +661,7 @@ export class Distribution extends Resource implements IDistribution {
661661
if (webAclId.startsWith('arn:')) {
662662
const webAclRegion = Stack.of(this).splitArn(webAclId, ArnFormat.SLASH_RESOURCE_NAME).region;
663663
if (!Token.isUnresolved(webAclRegion) && webAclRegion !== 'us-east-1') {
664-
throw new Error(`WebACL for CloudFront distributions must be created in the us-east-1 region; received ${webAclRegion}`);
664+
throw new ValidationError(`WebACL for CloudFront distributions must be created in the us-east-1 region; received ${webAclRegion}`, this);
665665
}
666666
}
667667
}
@@ -681,13 +681,13 @@ export class Distribution extends Resource implements IDistribution {
681681
const originId = originBindConfig.originProperty?.id ?? generatedId;
682682
const duplicateId = this.boundOrigins.find(boundOrigin => boundOrigin.originProperty?.id === originBindConfig.originProperty?.id);
683683
if (duplicateId) {
684-
throw new Error(`Origin with id ${duplicateId.originProperty?.id} already exists. OriginIds must be unique within a distribution`);
684+
throw new ValidationError(`Origin with id ${duplicateId.originProperty?.id} already exists. OriginIds must be unique within a distribution`, this);
685685
}
686686
if (!originBindConfig.failoverConfig) {
687687
this.boundOrigins.push({ origin, originId, distributionId, ...originBindConfig });
688688
} else {
689689
if (isFailoverOrigin) {
690-
throw new Error('An Origin cannot use an Origin with its own failover configuration as its fallback origin!');
690+
throw new ValidationError('An Origin cannot use an Origin with its own failover configuration as its fallback origin!', this);
691691
}
692692
const groupIndex = this.originGroups.length + 1;
693693
const originGroupId = Names.uniqueId(new Construct(this, `OriginGroup${groupIndex}`)).slice(-ORIGIN_ID_MAX_LENGTH);
@@ -716,7 +716,7 @@ export class Distribution extends Resource implements IDistribution {
716716
): void {
717717
statusCodes = statusCodes ?? [500, 502, 503, 504];
718718
if (statusCodes.length === 0) {
719-
throw new Error('fallbackStatusCodes cannot be empty');
719+
throw new ValidationError('fallbackStatusCodes cannot be empty', this);
720720
}
721721
this.originGroups.push({
722722
failoverCriteria: {
@@ -766,7 +766,7 @@ export class Distribution extends Resource implements IDistribution {
766766

767767
return this.errorResponses.map(errorConfig => {
768768
if (!errorConfig.responseHttpStatus && !errorConfig.ttl && !errorConfig.responsePagePath) {
769-
throw new Error('A custom error response without either a \'responseHttpStatus\', \'ttl\' or \'responsePagePath\' is not valid.');
769+
throw new ValidationError('A custom error response without either a \'responseHttpStatus\', \'ttl\' or \'responsePagePath\' is not valid.', this);
770770
}
771771

772772
return {
@@ -783,7 +783,7 @@ export class Distribution extends Resource implements IDistribution {
783783
private renderLogging(props: DistributionProps): CfnDistribution.LoggingProperty | undefined {
784784
if (!props.enableLogging && !props.logBucket) { return undefined; }
785785
if (props.enableLogging === false && props.logBucket) {
786-
throw new Error('Explicitly disabled logging but provided a logging bucket.');
786+
throw new ValidationError('Explicitly disabled logging but provided a logging bucket.', this);
787787
}
788788

789789
const bucket = props.logBucket ?? new s3.Bucket(this, 'LoggingBucket', {

packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Stack,
1313
Stage,
1414
Token,
15+
ValidationError,
1516
} from '../../../core';
1617
import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource';
1718
import { CrossRegionStringParamReaderProvider } from '../../../custom-resource-handlers/dist/aws-cloudfront/cross-region-string-param-reader-provider.generated';
@@ -104,10 +105,10 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
104105
* Not supported. Connections are only applicable to VPC-enabled functions.
105106
*/
106107
public get connections(): ec2.Connections {
107-
throw new Error('Lambda@Edge does not support connections');
108+
throw new ValidationError('Lambda@Edge does not support connections', this);
108109
}
109110
public get latestVersion(): lambda.IVersion {
110-
throw new Error('$LATEST function version cannot be used for Lambda@Edge');
111+
throw new ValidationError('$LATEST function version cannot be used for Lambda@Edge', this);
111112
}
112113

113114
@MethodMetadata()
@@ -188,7 +189,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
188189
private createCrossRegionFunction(id: string, props: EdgeFunctionProps): FunctionConfig {
189190
const parameterNamePrefix = 'cdk/EdgeFunctionArn';
190191
if (Token.isUnresolved(this.env.region)) {
191-
throw new Error('stacks which use EdgeFunctions must have an explicitly set region');
192+
throw new ValidationError('stacks which use EdgeFunctions must have an explicitly set region', this);
192193
}
193194
// SSM parameter names must only contain letters, numbers, ., _, -, or /.
194195
const sanitizedPath = this.node.path.replace(/[^\/\w.-]/g, '_');
@@ -254,7 +255,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion {
254255
private edgeStack(stackId?: string): Stack {
255256
const stage = Stage.of(this);
256257
if (!stage) {
257-
throw new Error('stacks which use EdgeFunctions must be part of a CDK app or stage');
258+
throw new ValidationError('stacks which use EdgeFunctions must be part of a CDK app or stage', this);
258259
}
259260

260261
const edgeStackId = stackId ?? `edge-lambda-stack-${this.stack.node.addr}`;

0 commit comments

Comments
 (0)