Skip to content

Commit 5e0f16d

Browse files
authored
feat(route53): throw ValidationError instead of untyped errors (#33110)
### Issue `aws-route53*` for #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 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 34ae997 commit 5e0f16d

10 files changed

+48
-32
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ const enableNoThrowDefaultErrorIn = [
2727
'aws-ssmquicksetup',
2828
'aws-apigatewayv2-authorizers',
2929
'aws-synthetics',
30+
'aws-route53',
31+
'aws-route53-patterns',
32+
'aws-route53-targets',
33+
'aws-route53profiles',
34+
'aws-route53recoverycontrol',
35+
'aws-route53recoveryreadiness',
36+
'aws-route53resolver',
3037
'aws-s3-assets',
3138
'aws-s3-deployment',
3239
'aws-s3-notifications',

packages/aws-cdk-lib/aws-route53-patterns/lib/website-redirect.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ARecord, AaaaRecord, IHostedZone, RecordTarget } from '../../aws-route5
55
import { CloudFrontTarget } from '../../aws-route53-targets';
66
import { BlockPublicAccess, Bucket, RedirectProtocol } from '../../aws-s3';
77
import { ArnFormat, RemovalPolicy, Stack, Token, FeatureFlags } from '../../core';
8+
import { ValidationError } from '../../core/lib/errors';
89
import { md5hash } from '../../core/lib/helpers-internal';
910
import { ROUTE53_PATTERNS_USE_CERTIFICATE } from '../../cx-api';
1011

@@ -61,7 +62,7 @@ export class HttpsRedirect extends Construct {
6162
if (props.certificate) {
6263
const certificateRegion = Stack.of(this).splitArn(props.certificate.certificateArn, ArnFormat.SLASH_RESOURCE_NAME).region;
6364
if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') {
64-
throw new Error(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`);
65+
throw new ValidationError(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`, this);
6566
}
6667
}
6768
const redirectCert = props.certificate ?? this.createCertificate(domainNames, props.zone);
@@ -123,10 +124,10 @@ export class HttpsRedirect extends Construct {
123124
const stack = Stack.of(this);
124125
const parent = stack.node.scope;
125126
if (!parent) {
126-
throw new Error(`Stack ${stack.stackId} must be created in the scope of an App or Stage`);
127+
throw new ValidationError(`Stack ${stack.stackId} must be created in the scope of an App or Stage`, this);
127128
}
128129
if (Token.isUnresolved(stack.region)) {
129-
throw new Error(`When ${ROUTE53_PATTERNS_USE_CERTIFICATE} is enabled, a region must be defined on the Stack`);
130+
throw new ValidationError(`When ${ROUTE53_PATTERNS_USE_CERTIFICATE} is enabled, a region must be defined on the Stack`, this);
130131
}
131132
if (stack.region !== 'us-east-1') {
132133
const stackId = `certificate-redirect-stack-${stack.node.addr}`;

packages/aws-cdk-lib/aws-route53-targets/lib/api-gateway-domain-name.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as apig from '../../aws-apigateway';
22
import * as route53 from '../../aws-route53';
3+
import { ValidationError } from '../../core/lib/errors';
34

45
/**
56
* Defines an API Gateway domain name as the alias target.
@@ -28,7 +29,7 @@ export class ApiGatewayDomain implements route53.IAliasRecordTarget {
2829
export class ApiGateway extends ApiGatewayDomain {
2930
constructor(api: apig.RestApiBase) {
3031
if (!api.domainName) {
31-
throw new Error('API does not define a default domain name');
32+
throw new ValidationError('API does not define a default domain name', api);
3233
}
3334

3435
super(api.domainName);

packages/aws-cdk-lib/aws-route53-targets/lib/bucket-website-target.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IAliasRecordTargetProps } from './shared';
22
import * as route53 from '../../aws-route53';
33
import * as s3 from '../../aws-s3';
44
import { Stack, Token } from '../../core';
5+
import { ValidationError } from '../../core/lib/errors';
56
import { RegionInfo } from '../../region-info';
67

78
/**
@@ -10,21 +11,21 @@ import { RegionInfo } from '../../region-info';
1011
export class BucketWebsiteTarget implements route53.IAliasRecordTarget {
1112
constructor(private readonly bucket: s3.IBucket, private readonly props?: IAliasRecordTargetProps) {}
1213

13-
public bind(_record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
14+
public bind(record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
1415
const { region } = Stack.of(this.bucket.stack);
1516

1617
if (Token.isUnresolved(region)) {
17-
throw new Error([
18+
throw new ValidationError([
1819
'Cannot use an S3 record alias in region-agnostic stacks.',
1920
'You must specify a specific region when you define the stack',
2021
'(see https://docs.aws.amazon.com/cdk/latest/guide/environments.html)',
21-
].join(' '));
22+
].join(' '), record);
2223
}
2324

2425
const { s3StaticWebsiteHostedZoneId: hostedZoneId, s3StaticWebsiteEndpoint: dnsName } = RegionInfo.get(region);
2526

2627
if (!hostedZoneId || !dnsName) {
27-
throw new Error(`Bucket website target is not supported for the "${region}" region`);
28+
throw new ValidationError(`Bucket website target is not supported for the "${region}" region`, record);
2829
}
2930

3031
return { hostedZoneId, dnsName, evaluateTargetHealth: this.props?.evaluateTargetHealth };

packages/aws-cdk-lib/aws-route53-targets/lib/elastic-beanstalk-environment-target.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IAliasRecordTargetProps } from './shared';
22
import * as route53 from '../../aws-route53';
33
import * as cdk from '../../core';
4+
import { ValidationError } from '../../core/lib/errors';
45
import { RegionInfo } from '../../region-info';
56

67
/**
@@ -13,9 +14,9 @@ import { RegionInfo } from '../../region-info';
1314
export class ElasticBeanstalkEnvironmentEndpointTarget implements route53.IAliasRecordTarget {
1415
constructor( private readonly environmentEndpoint: string, private readonly props?: IAliasRecordTargetProps) {}
1516

16-
public bind(_record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
17+
public bind(record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
1718
if (cdk.Token.isUnresolved(this.environmentEndpoint)) {
18-
throw new Error('Cannot use an EBS alias as `environmentEndpoint`. You must find your EBS environment endpoint via the AWS console. See the Elastic Beanstalk developer guide: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customdomains.html');
19+
throw new ValidationError('Cannot use an EBS alias as `environmentEndpoint`. You must find your EBS environment endpoint via the AWS console. See the Elastic Beanstalk developer guide: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customdomains.html', record);
1920
}
2021

2122
const dnsName = this.environmentEndpoint;
@@ -25,7 +26,7 @@ export class ElasticBeanstalkEnvironmentEndpointTarget implements route53.IAlias
2526
const { ebsEnvEndpointHostedZoneId: hostedZoneId } = RegionInfo.get(region);
2627

2728
if (!hostedZoneId || !dnsName) {
28-
throw new Error(`Elastic Beanstalk environment target is not supported for the "${region}" region.`);
29+
throw new ValidationError(`Elastic Beanstalk environment target is not supported for the "${region}" region.`, record);
2930
}
3031

3132
return {

packages/aws-cdk-lib/aws-route53-targets/lib/route53-record.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as route53 from '../../aws-route53';
2+
import { ValidationError } from '../../core/lib/errors';
23

34
/**
45
* Use another Route 53 record as an alias record target
@@ -7,9 +8,9 @@ export class Route53RecordTarget implements route53.IAliasRecordTarget {
78
constructor(private readonly record: route53.IRecordSet) {
89
}
910

10-
public bind(_record: route53.IRecordSet, zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
11+
public bind(record: route53.IRecordSet, zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
1112
if (!zone) { // zone introduced as optional to avoid a breaking change
12-
throw new Error('Cannot bind to record without a zone');
13+
throw new ValidationError('Cannot bind to record without a zone', record);
1314
}
1415
return {
1516
dnsName: this.record.domainName,

packages/aws-cdk-lib/aws-route53/lib/geo-location.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { UnscopedValidationError } from '../../core/lib/errors';
2+
13
/**
24
* Routing based on geographical location.
35
*/
@@ -49,21 +51,21 @@ export class GeoLocation {
4951
private static validateCountry(country: string) {
5052
if (!GeoLocation.COUNTRY_REGEX.test(country)) {
5153
// eslint-disable-next-line max-len
52-
throw new Error(`Invalid country format for country: ${country}, country should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
54+
throw new UnscopedValidationError(`Invalid country format for country: ${country}, country should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
5355
}
5456
}
5557

5658
private static validateCountryForSubdivision(country: string) {
5759
if (!GeoLocation.COUNTRY_FOR_SUBDIVISION_REGEX.test(country)) {
5860
// eslint-disable-next-line max-len
59-
throw new Error(`Invalid country for subdivisions geolocation: ${country}, only UA (Ukraine) and US (United states) are supported`);
61+
throw new UnscopedValidationError(`Invalid country for subdivisions geolocation: ${country}, only UA (Ukraine) and US (United states) are supported`);
6062
}
6163
}
6264

6365
private static validateSubDivision(subDivision: string) {
6466
if (!GeoLocation.SUBDIVISION_REGEX.test(subDivision)) {
6567
// eslint-disable-next-line max-len
66-
throw new Error(`Invalid subdivision format for subdivision: ${subDivision}, subdivision should be alphanumeric and between 1 and 3 characters`);
68+
throw new UnscopedValidationError(`Invalid subdivision format for subdivision: ${subDivision}, subdivision should be alphanumeric and between 1 and 3 characters`);
6769
}
6870
}
6971

packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as iam from '../../aws-iam';
1010
import * as kms from '../../aws-kms';
1111
import * as cxschema from '../../cloud-assembly-schema';
1212
import { ContextProvider, Duration, Lazy, Resource, Stack } from '../../core';
13+
import { ValidationError } from '../../core/lib/errors';
1314

1415
/**
1516
* Common properties to create a Route 53 hosted zone
@@ -105,7 +106,7 @@ export class HostedZone extends Resource implements IHostedZone {
105106
class Import extends Resource implements IHostedZone {
106107
public readonly hostedZoneId = hostedZoneId;
107108
public get zoneName(): string {
108-
throw new Error('Cannot reference `zoneName` when using `HostedZone.fromHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromHostedZoneAttributes()` or `fromLookup()` instead.');
109+
throw new ValidationError('Cannot reference `zoneName` when using `HostedZone.fromHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromHostedZoneAttributes()` or `fromLookup()` instead.', this);
109110
}
110111
public get hostedZoneArn(): string {
111112
return makeHostedZoneArn(this, this.hostedZoneId);
@@ -152,7 +153,7 @@ export class HostedZone extends Resource implements IHostedZone {
152153
*/
153154
public static fromLookup(scope: Construct, id: string, query: HostedZoneProviderProps): IHostedZone {
154155
if (!query.domainName) {
155-
throw new Error('Cannot use undefined value for attribute `domainName`');
156+
throw new ValidationError('Cannot use undefined value for attribute `domainName`', scope);
156157
}
157158

158159
const DEFAULT_HOSTED_ZONE: HostedZoneContextResponse = {
@@ -242,7 +243,7 @@ export class HostedZone extends Resource implements IHostedZone {
242243
*/
243244
public enableDnssec(options: ZoneSigningOptions): IKeySigningKey {
244245
if (this.keySigningKey) {
245-
throw new Error('DNSSEC is already enabled for this hosted zone');
246+
throw new ValidationError('DNSSEC is already enabled for this hosted zone', this);
246247
}
247248
this.keySigningKey = new KeySigningKey(this, 'KeySigningKey', {
248249
hostedZone: this,
@@ -323,7 +324,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
323324
public static fromPublicHostedZoneId(scope: Construct, id: string, publicHostedZoneId: string): IPublicHostedZone {
324325
class Import extends Resource implements IPublicHostedZone {
325326
public readonly hostedZoneId = publicHostedZoneId;
326-
public get zoneName(): string { throw new Error('Cannot reference `zoneName` when using `PublicHostedZone.fromPublicHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromPublicHostedZoneAttributes()` instead'); }
327+
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PublicHostedZone.fromPublicHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromPublicHostedZoneAttributes()` instead', this); }
327328
public get hostedZoneArn(): string {
328329
return makeHostedZoneArn(this, this.hostedZoneId);
329330
}
@@ -404,7 +405,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
404405
}
405406

406407
public addVpc(_vpc: ec2.IVpc) {
407-
throw new Error('Cannot associate public hosted zones with a VPC');
408+
throw new ValidationError('Cannot associate public hosted zones with a VPC', this);
408409
}
409410

410411
/**
@@ -483,7 +484,7 @@ export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone
483484
public static fromPrivateHostedZoneId(scope: Construct, id: string, privateHostedZoneId: string): IPrivateHostedZone {
484485
class Import extends Resource implements IPrivateHostedZone {
485486
public readonly hostedZoneId = privateHostedZoneId;
486-
public get zoneName(): string { throw new Error('Cannot reference `zoneName` when using `PrivateHostedZone.fromPrivateHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`'); }
487+
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PrivateHostedZone.fromPrivateHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`', this); }
487488
public get hostedZoneArn(): string {
488489
return makeHostedZoneArn(this, this.hostedZoneId);
489490
}

packages/aws-cdk-lib/aws-route53/lib/record-set.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CfnRecordSet } from './route53.generated';
77
import { determineFullyQualifiedDomainName } from './util';
88
import * as iam from '../../aws-iam';
99
import { CustomResource, Duration, IResource, Names, RemovalPolicy, Resource, Token } from '../../core';
10+
import { ValidationError } from '../../core/lib/errors';
1011
import { CrossAccountZoneDelegationProvider } from '../../custom-resource-handlers/dist/aws-route53/cross-account-zone-delegation-provider.generated';
1112
import { DeleteExistingRecordSetProvider } from '../../custom-resource-handlers/dist/aws-route53/delete-existing-record-set-provider.generated';
1213

@@ -340,16 +341,16 @@ export class RecordSet extends Resource implements IRecordSet {
340341
super(scope, id);
341342

342343
if (props.weight && !Token.isUnresolved(props.weight) && (props.weight < 0 || props.weight > 255)) {
343-
throw new Error(`weight must be between 0 and 255 inclusive, got: ${props.weight}`);
344+
throw new ValidationError(`weight must be between 0 and 255 inclusive, got: ${props.weight}`, this);
344345
}
345346
if (props.setIdentifier && (props.setIdentifier.length < 1 || props.setIdentifier.length > 128)) {
346-
throw new Error(`setIdentifier must be between 1 and 128 characters long, got: ${props.setIdentifier.length}`);
347+
throw new ValidationError(`setIdentifier must be between 1 and 128 characters long, got: ${props.setIdentifier.length}`, this);
347348
}
348349
if (props.setIdentifier && props.weight === undefined && !props.geoLocation && !props.region && !props.multiValueAnswer) {
349-
throw new Error('setIdentifier can only be specified for non-simple routing policies');
350+
throw new ValidationError('setIdentifier can only be specified for non-simple routing policies', this);
350351
}
351352
if (props.multiValueAnswer && props.target.aliasTarget) {
352-
throw new Error('multiValueAnswer cannot be specified for alias record');
353+
throw new ValidationError('multiValueAnswer cannot be specified for alias record', this);
353354
}
354355

355356
const nonSimpleRoutingPolicies = [
@@ -359,7 +360,7 @@ export class RecordSet extends Resource implements IRecordSet {
359360
props.multiValueAnswer,
360361
].filter((variable) => variable !== undefined).length;
361362
if (nonSimpleRoutingPolicies > 1) {
362-
throw new Error('Only one of region, weight, multiValueAnswer or geoLocation can be defined');
363+
throw new ValidationError('Only one of region, weight, multiValueAnswer or geoLocation can be defined', this);
363364
}
364365

365366
this.geoLocation = props.geoLocation;
@@ -546,9 +547,9 @@ class ARecordAsAliasTarget implements IAliasRecordTarget {
546547
constructor(private readonly aRrecordAttrs: ARecordAttrs) {
547548
}
548549

549-
public bind(_record: IRecordSet, _zone?: IHostedZone | undefined): AliasRecordTargetConfig {
550-
if (!_zone) {
551-
throw new Error('Cannot bind to record without a zone');
550+
public bind(record: IRecordSet, zone?: IHostedZone | undefined): AliasRecordTargetConfig {
551+
if (!zone) {
552+
throw new ValidationError('Cannot bind to record without a zone', record);
552553
}
553554
return {
554555
dnsName: this.aRrecordAttrs.targetDNS,

packages/aws-cdk-lib/aws-route53/lib/vpc-endpoint-service-domain-name.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Construct } from 'constructs';
22
import { IVpcEndpointService } from '../../aws-ec2';
33
import { Fn, Names, Stack } from '../../core';
4+
import { ValidationError } from '../../core/lib/errors';
45
import { md5hash } from '../../core/lib/helpers-internal';
56
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '../../custom-resources';
67
import { IPublicHostedZone, TxtRecord } from '../lib';
@@ -80,8 +81,7 @@ export class VpcEndpointServiceDomainName extends Construct {
8081
const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node);
8182
if (serviceUniqueId in VpcEndpointServiceDomainName.endpointServicesMap) {
8283
const endpoint = VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId];
83-
throw new Error(
84-
`Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`);
84+
throw new ValidationError(`Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`, this);
8585
}
8686
}
8787

0 commit comments

Comments
 (0)