Skip to content

Commit 04efe6c

Browse files
authored
feat(apigateway): throw ValidationError instead of untyped errors (#33075)
### Issue `aws-apigateway` 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 1beaf83 commit 04efe6c

20 files changed

+103
-84
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [
1818
const enableNoThrowDefaultErrorIn = [
1919
'aws-amplify',
2020
'aws-amplifyuibuilder',
21+
'aws-apigateway',
22+
'aws-apigatewayv2',
2123
'aws-apigatewayv2-authorizers',
2224
'aws-apigatewayv2-integrations',
2325
'aws-cognito',
@@ -34,8 +36,6 @@ const enableNoThrowDefaultErrorIn = [
3436
'aws-ssmcontacts',
3537
'aws-ssmincidents',
3638
'aws-ssmquicksetup',
37-
'aws-apigatewayv2',
38-
'aws-apigatewayv2-authorizers',
3939
'aws-synthetics',
4040
'aws-route53',
4141
'aws-route53-patterns',

packages/aws-cdk-lib/aws-apigateway/lib/access-log.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IStage } from './stage';
22
import * as firehose from '../../aws-kinesisfirehose';
33
import { ILogGroup } from '../../aws-logs';
4+
import { ValidationError } from '../../core/lib/errors';
45

56
/**
67
* Access log destination for a RestApi Stage.
@@ -49,9 +50,9 @@ export class FirehoseLogDestination implements IAccessLogDestination {
4950
/**
5051
* Binds this destination to the Firehose delivery stream.
5152
*/
52-
public bind(_stage: IStage): AccessLogDestinationConfig {
53+
public bind(stage: IStage): AccessLogDestinationConfig {
5354
if (!this.stream.deliveryStreamName?.startsWith('amazon-apigateway-')) {
54-
throw new Error(`Firehose delivery stream name for access log destination must begin with 'amazon-apigateway-', got '${this.stream.deliveryStreamName}'`);
55+
throw new ValidationError(`Firehose delivery stream name for access log destination must begin with 'amazon-apigateway-', got '${this.stream.deliveryStreamName}'`, stage);
5556
}
5657
return {
5758
destinationArn: this.stream.attrArn,

packages/aws-cdk-lib/aws-apigateway/lib/api-definition.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CfnRestApi } from './apigateway.generated';
33
import { IRestApi } from './restapi';
44
import * as s3 from '../../aws-s3';
55
import * as s3_assets from '../../aws-s3-assets';
6+
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
67
import * as cxapi from '../../cx-api';
78

89
/**
@@ -137,7 +138,7 @@ export class S3ApiDefinition extends ApiDefinition {
137138
super();
138139

139140
if (!bucket.bucketName) {
140-
throw new Error('bucketName is undefined for the provided bucket');
141+
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
141142
}
142143

143144
this.bucketName = bucket.bucketName;
@@ -162,11 +163,11 @@ export class InlineApiDefinition extends ApiDefinition {
162163
super();
163164

164165
if (typeof(definition) !== 'object') {
165-
throw new Error('definition should be of type object');
166+
throw new UnscopedValidationError('definition should be of type object');
166167
}
167168

168169
if (Object.keys(definition).length === 0) {
169-
throw new Error('JSON definition cannot be empty');
170+
throw new UnscopedValidationError('JSON definition cannot be empty');
170171
}
171172
}
172173

@@ -197,7 +198,7 @@ export class AssetApiDefinition extends ApiDefinition {
197198
}
198199

199200
if (this.asset.isZipArchive) {
200-
throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`);
201+
throw new ValidationError(`Asset cannot be a .zip file or a directory (${this.path})`, scope);
201202
}
202203

203204
return {
@@ -214,7 +215,7 @@ export class AssetApiDefinition extends ApiDefinition {
214215
}
215216

216217
if (!this.asset) {
217-
throw new Error('bindToResource() must be called after bind()');
218+
throw new ValidationError('bindToResource() must be called after bind()', scope);
218219
}
219220

220221
const child = Node.of(restApi).defaultChild as CfnRestApi;

packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IStage } from './stage';
66
import { QuotaSettings, ThrottleSettings, UsagePlan, UsagePlanPerApiStage } from './usage-plan';
77
import * as iam from '../../aws-iam';
88
import { ArnFormat, IResource as IResourceBase, Resource, Stack } from '../../core';
9+
import { ValidationError } from '../../core/lib/errors';
910

1011
/**
1112
* API keys are alphanumeric string values that you distribute to
@@ -196,15 +197,15 @@ export class ApiKey extends ApiKeyBase {
196197
}
197198

198199
if (resources && stages) {
199-
throw new Error('Only one of "resources" or "stages" should be provided');
200+
throw new ValidationError('Only one of "resources" or "stages" should be provided', this);
200201
}
201202

202203
return resources
203204
? resources.map((resource: IRestApi) => {
204205
const restApi = resource;
205206
if (!restApi.deploymentStage) {
206-
throw new Error('Cannot add an ApiKey to a RestApi that does not contain a "deploymentStage".\n'+
207-
'Either set the RestApi.deploymentStage or create an ApiKey from a Stage');
207+
throw new ValidationError('Cannot add an ApiKey to a RestApi that does not contain a "deploymentStage".\n'+
208+
'Either set the RestApi.deploymentStage or create an ApiKey from a Stage', this);
208209
}
209210
const restApiId = restApi.restApiId;
210211
const stageName = restApi.deploymentStage!.stageName.toString();

packages/aws-cdk-lib/aws-apigateway/lib/authorizers/cognito.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Construct } from 'constructs';
22
import { IdentitySource } from './identity-source';
33
import * as cognito from '../../../aws-cognito';
44
import { Duration, FeatureFlags, Lazy, Names, Stack } from '../../../core';
5+
import { ValidationError } from '../../../core/lib/errors';
56
import { APIGATEWAY_AUTHORIZER_CHANGE_DEPLOYMENT_LOGICAL_ID } from '../../../cx-api';
67
import { CfnAuthorizer, CfnAuthorizerProps } from '../apigateway.generated';
78
import { Authorizer, IAuthorizer } from '../authorizer';
@@ -102,7 +103,7 @@ export class CognitoUserPoolsAuthorizer extends Authorizer implements IAuthorize
102103
*/
103104
public _attachToApi(restApi: IRestApi): void {
104105
if (this.restApiId && this.restApiId !== restApi.restApiId) {
105-
throw new Error('Cannot attach authorizer to two different rest APIs');
106+
throw new ValidationError('Cannot attach authorizer to two different rest APIs', restApi);
106107
}
107108

108109
this.restApiId = restApi.restApiId;
@@ -126,7 +127,7 @@ export class CognitoUserPoolsAuthorizer extends Authorizer implements IAuthorize
126127
return Lazy.string({
127128
produce: () => {
128129
if (!this.restApiId) {
129-
throw new Error(`Authorizer (${this.node.path}) must be attached to a RestApi`);
130+
throw new ValidationError(`Authorizer (${this.node.path}) must be attached to a RestApi`, this);
130131
}
131132
return this.restApiId;
132133
},

packages/aws-cdk-lib/aws-apigateway/lib/authorizers/identity-source.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { UnscopedValidationError } from '../../../core/lib/errors';
2+
13
/**
24
* Represents an identity source.
35
*
@@ -47,7 +49,7 @@ export class IdentitySource {
4749

4850
private static toString(source: string, type: string) {
4951
if (!source.trim()) {
50-
throw new Error('IdentitySources must be a non-empty string.');
52+
throw new UnscopedValidationError('IdentitySources must be a non-empty string.');
5153
}
5254

5355
return `${type}.${source}`;

packages/aws-cdk-lib/aws-apigateway/lib/authorizers/lambda.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IdentitySource } from './identity-source';
33
import * as iam from '../../../aws-iam';
44
import * as lambda from '../../../aws-lambda';
55
import { Arn, ArnFormat, Duration, FeatureFlags, Lazy, Names, Stack } from '../../../core';
6+
import { ValidationError } from '../../../core/lib/errors';
67
import { APIGATEWAY_AUTHORIZER_CHANGE_DEPLOYMENT_LOGICAL_ID } from '../../../cx-api';
78
import { CfnAuthorizer, CfnAuthorizerProps } from '../apigateway.generated';
89
import { Authorizer, IAuthorizer } from '../authorizer';
@@ -79,7 +80,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer {
7980
this.role = props.assumeRole;
8081

8182
if (props.resultsCacheTtl && props.resultsCacheTtl?.toSeconds() > 3600) {
82-
throw new Error('Lambda authorizer property \'resultsCacheTtl\' must not be greater than 3600 seconds (1 hour)');
83+
throw new ValidationError('Lambda authorizer property \'resultsCacheTtl\' must not be greater than 3600 seconds (1 hour)', scope);
8384
}
8485
}
8586

@@ -89,7 +90,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer {
8990
*/
9091
public _attachToApi(restApi: IRestApi) {
9192
if (this.restApiId && this.restApiId !== restApi.restApiId) {
92-
throw new Error('Cannot attach authorizer to two different rest APIs');
93+
throw new ValidationError('Cannot attach authorizer to two different rest APIs', this);
9394
}
9495

9596
this.restApiId = restApi.restApiId;
@@ -160,7 +161,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer {
160161
return Lazy.string({
161162
produce: () => {
162163
if (!this.restApiId) {
163-
throw new Error(`Authorizer (${this.node.path}) must be attached to a RestApi`);
164+
throw new ValidationError(`Authorizer (${this.node.path}) must be attached to a RestApi`, this);
164165
}
165166
return this.restApiId;
166167
},
@@ -272,7 +273,7 @@ export class RequestAuthorizer extends LambdaAuthorizer {
272273
super(scope, id, props);
273274

274275
if ((props.resultsCacheTtl === undefined || props.resultsCacheTtl.toSeconds() !== 0) && props.identitySources.length === 0) {
275-
throw new Error('At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.');
276+
throw new ValidationError('At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.', scope);
276277
}
277278

278279
const restApiId = this.lazyRestApiId();

packages/aws-cdk-lib/aws-apigateway/lib/base-path-mapping.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IDomainName } from './domain-name';
44
import { IRestApi, RestApiBase } from './restapi';
55
import { Stage } from './stage';
66
import { Resource, Token } from '../../core';
7+
import { ValidationError } from '../../core/lib/errors';
78

89
export interface BasePathMappingOptions {
910
/**
@@ -57,13 +58,13 @@ export class BasePathMapping extends Resource {
5758

5859
if (props.basePath && !Token.isUnresolved(props.basePath)) {
5960
if (props.basePath.startsWith('/') || props.basePath.endsWith('/')) {
60-
throw new Error(`A base path cannot start or end with /", received: ${props.basePath}`);
61+
throw new ValidationError(`A base path cannot start or end with /", received: ${props.basePath}`, scope);
6162
}
6263
if (props.basePath.match(/\/{2,}/)) {
63-
throw new Error(`A base path cannot have more than one consecutive /", received: ${props.basePath}`);
64+
throw new ValidationError(`A base path cannot have more than one consecutive /", received: ${props.basePath}`, scope);
6465
}
6566
if (!props.basePath.match(/^[a-zA-Z0-9$_.+!*'()-/]+$/)) {
66-
throw new Error(`A base path may only contain letters, numbers, and one of "$-_.+!*'()/", received: ${props.basePath}`);
67+
throw new ValidationError(`A base path may only contain letters, numbers, and one of "$-_.+!*'()/", received: ${props.basePath}`, scope);
6768
}
6869
}
6970

packages/aws-cdk-lib/aws-apigateway/lib/deployment.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CfnDeployment } from './apigateway.generated';
33
import { Method } from './method';
44
import { IRestApi, RestApi, SpecRestApi, RestApiBase } from './restapi';
55
import { Lazy, RemovalPolicy, Resource, CfnResource } from '../../core';
6+
import { ValidationError } from '../../core/lib/errors';
67
import { md5hash } from '../../core/lib/helpers-internal';
78

89
export interface DeploymentProps {
@@ -168,7 +169,7 @@ class LatestDeploymentResource extends CfnDeployment {
168169
// if the construct is locked, it means we are already synthesizing and then
169170
// we can't modify the hash because we might have already calculated it.
170171
if (this.node.locked) {
171-
throw new Error('Cannot modify the logical ID when the construct is locked');
172+
throw new ValidationError('Cannot modify the logical ID when the construct is locked', this);
172173
}
173174

174175
this.hashComponents.push(data);

packages/aws-cdk-lib/aws-apigateway/lib/domain-name.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as apigwv2 from '../../aws-apigatewayv2';
77
import * as acm from '../../aws-certificatemanager';
88
import { IBucket } from '../../aws-s3';
99
import { IResource, Names, Resource, Token } from '../../core';
10+
import { ValidationError } from '../../core/lib/errors';
1011

1112
/**
1213
* Options for creating an api mapping
@@ -143,7 +144,7 @@ export class DomainName extends Resource implements IDomainName {
143144
this.securityPolicy = props.securityPolicy;
144145

145146
if (!Token.isUnresolved(props.domainName) && /[A-Z]/.test(props.domainName)) {
146-
throw new Error(`Domain name does not support uppercase letters. Got: ${props.domainName}`);
147+
throw new ValidationError(`Domain name does not support uppercase letters. Got: ${props.domainName}`, scope);
147148
}
148149

149150
const mtlsConfig = this.configureMTLS(props.mtls);
@@ -181,10 +182,10 @@ export class DomainName extends Resource implements IDomainName {
181182
private validateBasePath(path?: string): boolean {
182183
if (this.isMultiLevel(path)) {
183184
if (this.endpointType === EndpointType.EDGE) {
184-
throw new Error('multi-level basePath is only supported when endpointType is EndpointType.REGIONAL');
185+
throw new ValidationError('multi-level basePath is only supported when endpointType is EndpointType.REGIONAL', this);
185186
}
186187
if (this.securityPolicy && this.securityPolicy !== SecurityPolicy.TLS_1_2) {
187-
throw new Error('securityPolicy must be set to TLS_1_2 if multi-level basePath is provided');
188+
throw new ValidationError('securityPolicy must be set to TLS_1_2 if multi-level basePath is provided', this);
188189
}
189190
return true;
190191
}
@@ -207,10 +208,10 @@ export class DomainName extends Resource implements IDomainName {
207208
*/
208209
public addBasePathMapping(targetApi: IRestApi, options: BasePathMappingOptions = { }): BasePathMapping {
209210
if (this.basePaths.has(options.basePath)) {
210-
throw new Error(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`);
211+
throw new ValidationError(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`, this);
211212
}
212213
if (this.isMultiLevel(options.basePath)) {
213-
throw new Error('BasePathMapping does not support multi-level paths. Use "addApiMapping instead.');
214+
throw new ValidationError('BasePathMapping does not support multi-level paths. Use "addApiMapping instead.', this);
214215
}
215216

216217
this.basePaths.add(options.basePath);
@@ -236,7 +237,7 @@ export class DomainName extends Resource implements IDomainName {
236237
*/
237238
public addApiMapping(targetStage: IStage, options: ApiMappingOptions = {}): void {
238239
if (this.basePaths.has(options.basePath)) {
239-
throw new Error(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`);
240+
throw new ValidationError(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`, this);
240241
}
241242
this.validateBasePath(options.basePath);
242243
this.basePaths.add(options.basePath);

packages/aws-cdk-lib/aws-apigateway/lib/integration.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Method } from './method';
22
import { IVpcLink, VpcLink } from './vpc-link';
33
import * as iam from '../../aws-iam';
44
import { Lazy, Duration } from '../../core';
5+
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
56

67
export interface IntegrationOptions {
78
/**
@@ -199,31 +200,31 @@ export class Integration {
199200
constructor(private readonly props: IntegrationProps) {
200201
const options = this.props.options || { };
201202
if (options.credentialsPassthrough !== undefined && options.credentialsRole !== undefined) {
202-
throw new Error('\'credentialsPassthrough\' and \'credentialsRole\' are mutually exclusive');
203+
throw new UnscopedValidationError('\'credentialsPassthrough\' and \'credentialsRole\' are mutually exclusive');
203204
}
204205

205206
if (options.connectionType === ConnectionType.VPC_LINK && options.vpcLink === undefined) {
206-
throw new Error('\'connectionType\' of VPC_LINK requires \'vpcLink\' prop to be set');
207+
throw new UnscopedValidationError('\'connectionType\' of VPC_LINK requires \'vpcLink\' prop to be set');
207208
}
208209

209210
if (options.connectionType === ConnectionType.INTERNET && options.vpcLink !== undefined) {
210-
throw new Error('cannot set \'vpcLink\' where \'connectionType\' is INTERNET');
211+
throw new UnscopedValidationError('cannot set \'vpcLink\' where \'connectionType\' is INTERNET');
211212
}
212213

213214
if (options.timeout && !options.timeout.isUnresolved() && options.timeout.toMilliseconds() < 50) {
214-
throw new Error('Integration timeout must be greater than 50 milliseconds.');
215+
throw new UnscopedValidationError('Integration timeout must be greater than 50 milliseconds.');
215216
}
216217

217218
if (props.type !== IntegrationType.MOCK && !props.integrationHttpMethod) {
218-
throw new Error('integrationHttpMethod is required for non-mock integration types.');
219+
throw new UnscopedValidationError('integrationHttpMethod is required for non-mock integration types.');
219220
}
220221
}
221222

222223
/**
223224
* Can be overridden by subclasses to allow the integration to interact with the method
224225
* being integrated, access the REST API object, method ARNs, etc.
225226
*/
226-
public bind(_method: Method): IntegrationConfig {
227+
public bind(method: Method): IntegrationConfig {
227228
let uri = this.props.uri;
228229
const options = this.props.options;
229230

@@ -235,12 +236,12 @@ export class Integration {
235236
if (vpcLink instanceof VpcLink) {
236237
const targets = vpcLink._targetDnsNames;
237238
if (targets.length > 1) {
238-
throw new Error("'uri' is required when there are more than one NLBs in the VPC Link");
239+
throw new ValidationError("'uri' is required when there are more than one NLBs in the VPC Link", method);
239240
} else {
240241
return `http://${targets[0]}`;
241242
}
242243
} else {
243-
throw new Error("'uri' is required when the 'connectionType' is VPC_LINK");
244+
throw new ValidationError("'uri' is required when the 'connectionType' is VPC_LINK", method);
244245
}
245246
},
246247
});

packages/aws-cdk-lib/aws-apigateway/lib/integrations/aws.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IConstruct } from 'constructs';
22
import * as cdk from '../../../core';
33
import { ArnFormat } from '../../../core';
4+
import { UnscopedValidationError } from '../../../core/lib/errors';
45
import { Integration, IntegrationConfig, IntegrationOptions, IntegrationType } from '../integration';
56
import { Method } from '../method';
67
import { parseAwsApiCall } from '../util';
@@ -89,7 +90,7 @@ export class AwsIntegration extends Integration {
8990
integrationHttpMethod: props.integrationHttpMethod || 'POST',
9091
uri: cdk.Lazy.string({
9192
produce: () => {
92-
if (!this.scope) { throw new Error('AwsIntegration must be used in API'); }
93+
if (!this.scope) { throw new UnscopedValidationError('AwsIntegration must be used in API'); }
9394
return cdk.Stack.of(this.scope).formatArn({
9495
service: 'apigateway',
9596
account: backend,

packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AwsIntegration } from './aws';
55
import * as iam from '../../../aws-iam';
66
import * as sfn from '../../../aws-stepfunctions';
77
import { Token } from '../../../core';
8+
import { ValidationError } from '../../../core/lib/errors';
89
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration';
910
import { Method } from '../method';
1011
import { Model } from '../model';
@@ -150,7 +151,7 @@ class StepFunctionsExecutionIntegration extends AwsIntegration {
150151
if (this.stateMachine instanceof sfn.StateMachine) {
151152
const stateMachineType = (this.stateMachine as sfn.StateMachine).stateMachineType;
152153
if (stateMachineType !== sfn.StateMachineType.EXPRESS) {
153-
throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType');
154+
throw new ValidationError('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType', method);
154155
}
155156

156157
// if not imported, extract the name from the CFN layer to reach the

0 commit comments

Comments
 (0)