Skip to content

Commit 8b472fc

Browse files
authored
feat(apigatewayv2): throw ValidationError instead of untyped errors (#33072)
### Issue `aws-apigatewayv2` 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 cc1988a commit 8b472fc

File tree

14 files changed

+55
-36
lines changed

14 files changed

+55
-36
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const enableNoThrowDefaultErrorIn = [
2424
'aws-elasticloadbalancingv2-targets',
2525
'aws-lambda',
2626
'aws-rds',
27+
'aws-s3',
28+
'aws-sns',
29+
'aws-sqs',
30+
'aws-ssm',
31+
'aws-ssmcontacts',
32+
'aws-ssmincidents',
33+
'aws-ssmquicksetup',
34+
'aws-apigatewayv2',
35+
'aws-apigatewayv2-authorizers',
36+
'aws-synthetics',
2737
'aws-route53',
2838
'aws-route53-patterns',
2939
'aws-route53-targets',

packages/aws-cdk-lib/aws-apigatewayv2/lib/common/api-mapping.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IDomainName } from './domain-name';
44
import { IStage } from './stage';
55
import { CfnApiMapping, CfnApiMappingProps } from '.././index';
66
import { IResource, Resource } from '../../../core';
7+
import { ValidationError } from '../../../core/lib/errors';
78

89
/**
910
* Represents an ApiGatewayV2 ApiMapping resource
@@ -95,11 +96,11 @@ export class ApiMapping extends Resource implements IApiMapping {
9596
// So casting to 'any'
9697
let stage = props.stage ?? (props.api as any).defaultStage;
9798
if (!stage) {
98-
throw new Error('stage property must be specified');
99+
throw new ValidationError('stage property must be specified', scope);
99100
}
100101

101102
if (props.apiMappingKey === '') {
102-
throw new Error('empty string for api mapping key not allowed');
103+
throw new ValidationError('empty string for api mapping key not allowed', scope);
103104
}
104105

105106
const apiMappingProps: CfnApiMappingProps = {

packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ApiMapping } from './api-mapping';
33
import { DomainMappingOptions, IStage } from './stage';
44
import * as cloudwatch from '../../../aws-cloudwatch';
55
import { Resource } from '../../../core';
6+
import { UnscopedValidationError } from '../../../core/lib/errors';
67

78
/**
89
* Base class representing an API
@@ -46,7 +47,7 @@ export abstract class StageBase extends Resource implements IStage {
4647
*/
4748
protected _addDomainMapping(domainMapping: DomainMappingOptions) {
4849
if (this._apiMapping) {
49-
throw new Error('Only one ApiMapping allowed per Stage');
50+
throw new UnscopedValidationError('Only one ApiMapping allowed per Stage');
5051
}
5152
this._apiMapping = new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, {
5253
api: this.baseApi,

packages/aws-cdk-lib/aws-apigatewayv2/lib/common/domain-name.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CfnDomainName, CfnDomainNameProps } from '.././index';
33
import { ICertificate } from '../../../aws-certificatemanager';
44
import { IBucket } from '../../../aws-s3';
55
import { IResource, Lazy, Resource, Token } from '../../../core';
6+
import { ValidationError } from '../../../core/lib/errors';
67

78
/**
89
* The minimum version of the SSL protocol that you want API Gateway to use for HTTPS connections.
@@ -172,12 +173,12 @@ export class DomainName extends Resource implements IDomainName {
172173
super(scope, id);
173174

174175
if (props.domainName === '') {
175-
throw new Error('empty string for domainName not allowed');
176+
throw new ValidationError('empty string for domainName not allowed', scope);
176177
}
177178

178179
// validation for ownership certificate
179180
if (props.ownershipCertificate && !props.mtls) {
180-
throw new Error('ownership certificate can only be used with mtls domains');
181+
throw new ValidationError('ownership certificate can only be used with mtls domains', scope);
181182
}
182183

183184
const mtlsConfig = this.configureMTLS(props.mtls);
@@ -225,7 +226,7 @@ export class DomainName extends Resource implements IDomainName {
225226
private validateEndpointType(endpointType: string | undefined) : void {
226227
for (let config of this.domainNameConfigurations) {
227228
if (endpointType && endpointType == config.endpointType) {
228-
throw new Error(`an endpoint with type ${endpointType} already exists`);
229+
throw new ValidationError(`an endpoint with type ${endpointType} already exists`, this);
229230
}
230231
}
231232
}

packages/aws-cdk-lib/aws-apigatewayv2/lib/http/api.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { VpcLink, VpcLinkProps } from './vpc-link';
77
import { CfnApi, CfnApiProps } from '.././index';
88
import { Metric, MetricOptions } from '../../../aws-cloudwatch';
99
import { ArnFormat, Duration, Stack, Token } from '../../../core';
10+
import { ValidationError } from '../../../core/lib/errors';
1011
import { IApi } from '../common/api';
1112
import { ApiBase } from '../common/base';
1213
import { DomainMappingOptions } from '../common/stage';
13-
1414
/**
1515
* Represents an HTTP API
1616
*/
@@ -314,7 +314,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th
314314

315315
public arnForExecuteApi(method?: string, path?: string, stage?: string): string {
316316
if (path && !Token.isUnresolved(path) && !path.startsWith('/')) {
317-
throw new Error(`Path must start with '/': ${path}`);
317+
throw new ValidationError(`Path must start with '/': ${path}`, this);
318318
}
319319

320320
if (method && method.toUpperCase() === 'ANY') {
@@ -363,7 +363,7 @@ export class HttpApi extends HttpApiBase {
363363

364364
public get apiEndpoint(): string {
365365
if (!this._apiEndpoint) {
366-
throw new Error('apiEndpoint is not configured on the imported HttpApi.');
366+
throw new ValidationError('apiEndpoint is not configured on the imported HttpApi.', scope);
367367
}
368368
return this._apiEndpoint;
369369
}
@@ -416,7 +416,7 @@ export class HttpApi extends HttpApiBase {
416416
if (props?.corsPreflight) {
417417
const cors = props.corsPreflight;
418418
if (cors.allowOrigins && cors.allowOrigins.includes('*') && cors.allowCredentials) {
419-
throw new Error("CORS preflight - allowCredentials is not supported when allowOrigin is '*'");
419+
throw new ValidationError("CORS preflight - allowCredentials is not supported when allowOrigin is '*'", scope);
420420
}
421421
const {
422422
allowCredentials,
@@ -476,8 +476,7 @@ export class HttpApi extends HttpApiBase {
476476
}
477477

478478
if (props?.createDefaultStage === false && props.defaultDomainMapping) {
479-
throw new Error('defaultDomainMapping not supported with createDefaultStage disabled',
480-
);
479+
throw new ValidationError('defaultDomainMapping not supported with createDefaultStage disabled', scope);
481480
}
482481
}
483482

@@ -486,7 +485,7 @@ export class HttpApi extends HttpApiBase {
486485
*/
487486
public get apiEndpoint(): string {
488487
if (this.disableExecuteApiEndpoint) {
489-
throw new Error('apiEndpoint is not accessible when disableExecuteApiEndpoint is set to true.');
488+
throw new ValidationError('apiEndpoint is not accessible when disableExecuteApiEndpoint is set to true.', this);
490489
}
491490
return this._apiEndpoint;
492491
}

packages/aws-cdk-lib/aws-apigatewayv2/lib/http/authorizer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IHttpApi } from './api';
33
import { IHttpRoute } from './route';
44
import { CfnAuthorizer } from '.././index';
55
import { Duration, Resource } from '../../../core';
6-
6+
import { ValidationError } from '../../../core/lib/errors';
77
import { IAuthorizer } from '../common';
88

99
/**
@@ -161,11 +161,11 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer {
161161
let authorizerPayloadFormatVersion = props.payloadFormatVersion;
162162

163163
if (props.type === HttpAuthorizerType.JWT && (!props.jwtAudience || props.jwtAudience.length === 0 || !props.jwtIssuer)) {
164-
throw new Error('jwtAudience and jwtIssuer are mandatory for JWT authorizers');
164+
throw new ValidationError('jwtAudience and jwtIssuer are mandatory for JWT authorizers', scope);
165165
}
166166

167167
if (props.type === HttpAuthorizerType.LAMBDA && !props.authorizerUri) {
168-
throw new Error('authorizerUri is mandatory for Lambda authorizers');
168+
throw new ValidationError('authorizerUri is mandatory for Lambda authorizers', scope);
169169
}
170170

171171
/**

packages/aws-cdk-lib/aws-apigatewayv2/lib/http/integration.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { HttpMethod, IHttpRoute } from './route';
44
import { CfnIntegration } from '.././index';
55
import { IRole } from '../../../aws-iam';
66
import { Aws, Duration, Resource } from '../../../core';
7+
import { ValidationError } from '../../../core/lib/errors';
78
import { IIntegration } from '../common';
89
import { ParameterMapping } from '../parameter-mapping';
910

@@ -254,11 +255,11 @@ export class HttpIntegration extends Resource implements IHttpIntegration {
254255
super(scope, id);
255256

256257
if (!props.integrationSubtype && !props.integrationUri) {
257-
throw new Error('Either `integrationSubtype` or `integrationUri` must be specified.');
258+
throw new ValidationError('Either `integrationSubtype` or `integrationUri` must be specified.', scope);
258259
}
259260

260261
if (props.timeout && !props.timeout.isUnresolved() && (props.timeout.toMilliseconds() < 50 || props.timeout.toMilliseconds() > 29000)) {
261-
throw new Error('Integration timeout must be between 50 milliseconds and 29 seconds.');
262+
throw new ValidationError('Integration timeout must be between 50 milliseconds and 29 seconds.', scope);
262263
}
263264

264265
const integ = new CfnIntegration(this, 'Resource', {
@@ -321,7 +322,7 @@ export abstract class HttpRouteIntegration {
321322
*/
322323
public _bindToRoute(options: HttpRouteIntegrationBindOptions): { readonly integrationId: string } {
323324
if (this.integration && this.integration.httpApi.node.addr !== options.route.httpApi.node.addr) {
324-
throw new Error('A single integration cannot be associated with multiple APIs.');
325+
throw new ValidationError('A single integration cannot be associated with multiple APIs.', options.scope);
325326
}
326327

327328
if (!this.integration) {

packages/aws-cdk-lib/aws-apigatewayv2/lib/http/route.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { HttpRouteIntegration } from './integration';
55
import { CfnRoute, CfnRouteProps } from '.././index';
66
import * as iam from '../../../aws-iam';
77
import { Aws, Resource } from '../../../core';
8+
import { UnscopedValidationError, ValidationError } from '../../../core/lib/errors';
89
import { IRoute } from '../common';
910

1011
/**
@@ -84,7 +85,7 @@ export class HttpRouteKey {
8485
*/
8586
public static with(path: string, method?: HttpMethod) {
8687
if (path !== '/' && (!path.startsWith('/') || path.endsWith('/'))) {
87-
throw new Error('A route path must always start with a "/" and not end with a "/"');
88+
throw new UnscopedValidationError('A route path must always start with a "/" and not end with a "/"');
8889
}
8990
return new HttpRouteKey(method, path);
9091
}
@@ -200,7 +201,7 @@ export class HttpRoute extends Resource implements IHttpRoute {
200201
});
201202

202203
if (this.authBindResult && !(this.authBindResult.authorizationType in HttpRouteAuthorizationType)) {
203-
throw new Error(`authorizationType should either be AWS_IAM, JWT, CUSTOM, or NONE but was '${this.authBindResult.authorizationType}'`);
204+
throw new ValidationError(`authorizationType should either be AWS_IAM, JWT, CUSTOM, or NONE but was '${this.authBindResult.authorizationType}'`, scope);
204205
}
205206

206207
let authorizationScopes = this.authBindResult?.authorizationScopes;
@@ -236,7 +237,7 @@ export class HttpRoute extends Resource implements IHttpRoute {
236237
// When the user has provided a path with path variables, we replace the
237238
// path variable and all that follows with a wildcard.
238239
if (path.length > 1000) {
239-
throw new Error(`Path is too long: ${path}`);
240+
throw new ValidationError(`Path is too long: ${path}`, this);
240241
};
241242
const iamPath = path.replace(/\{.*?\}.*/, '*');
242243

@@ -245,12 +246,12 @@ export class HttpRoute extends Resource implements IHttpRoute {
245246

246247
public grantInvoke(grantee: iam.IGrantable, options: GrantInvokeOptions = {}): iam.Grant {
247248
if (!this.authBindResult || this.authBindResult.authorizationType !== HttpRouteAuthorizationType.AWS_IAM) {
248-
throw new Error('To use grantInvoke, you must use IAM authorization');
249+
throw new ValidationError('To use grantInvoke, you must use IAM authorization', this);
249250
}
250251

251252
const httpMethods = Array.from(new Set(options.httpMethods ?? [this.method]));
252253
if (this.method !== HttpMethod.ANY && httpMethods.some(method => method !== this.method)) {
253-
throw new Error('This route does not support granting invoke for all requested http methods');
254+
throw new ValidationError('This route does not support granting invoke for all requested http methods', this);
254255
}
255256

256257
const resourceArns = httpMethods.map(httpMethod => {

packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IHttpApi } from './api';
33
import { CfnStage } from '.././index';
44
import { Metric, MetricOptions } from '../../../aws-cloudwatch';
55
import { Stack } from '../../../core';
6+
import { ValidationError } from '../../../core/lib/errors';
67
import { StageOptions, IStage, StageAttributes } from '../common';
78
import { IApi } from '../common/api';
89
import { StageBase } from '../common/base';
@@ -144,11 +145,11 @@ export class HttpStage extends HttpStageBase {
144145
public readonly api = attrs.api;
145146

146147
get url(): string {
147-
throw new Error('url is not available for imported stages.');
148+
throw new ValidationError('url is not available for imported stages.', scope);
148149
}
149150

150151
get domainUrl(): string {
151-
throw new Error('domainUrl is not available for imported stages.');
152+
throw new ValidationError('domainUrl is not available for imported stages.', scope);
152153
}
153154
}
154155
return new Import(scope, id);
@@ -194,7 +195,7 @@ export class HttpStage extends HttpStageBase {
194195

195196
public get domainUrl(): string {
196197
if (!this._apiMapping) {
197-
throw new Error('domainUrl is not available when no API mapping is associated with the Stage');
198+
throw new ValidationError('domainUrl is not available when no API mapping is associated with the Stage', this);
198199
}
199200

200201
return `https://${this._apiMapping.domainName.name}/${this._apiMapping.mappingKey ?? ''}`;

packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { WebSocketRoute, WebSocketRouteOptions } from './route';
33
import { CfnApi } from '.././index';
44
import { Grant, IGrantable } from '../../../aws-iam';
55
import { ArnFormat, Stack, Token } from '../../../core';
6+
import { UnscopedValidationError, ValidationError } from '../../../core/lib/errors';
67
import { IApi } from '../common/api';
78
import { ApiBase } from '../common/base';
89

@@ -116,7 +117,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {
116117

117118
public get apiEndpoint(): string {
118119
if (!this._apiEndpoint) {
119-
throw new Error('apiEndpoint is not configured on the imported WebSocketApi.');
120+
throw new ValidationError('apiEndpoint is not configured on the imported WebSocketApi.', scope);
120121
}
121122
return this._apiEndpoint;
122123
}
@@ -200,7 +201,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {
200201
*/
201202
public arnForExecuteApi(method?: string, path?: string, stage?: string): string {
202203
if (path && !Token.isUnresolved(path) && !path.startsWith('/')) {
203-
throw new Error(`Path must start with '/': ${path}`);
204+
throw new UnscopedValidationError(`Path must start with '/': ${path}`);
204205
}
205206

206207
if (method && method.toUpperCase() === 'ANY') {

packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/authorizer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IWebSocketApi } from './api';
33
import { IWebSocketRoute } from './route';
44
import { CfnAuthorizer } from '.././index';
55
import { Resource } from '../../../core';
6-
6+
import { ValidationError } from '../../../core/lib/errors';
77
import { IAuthorizer } from '../common';
88

99
/**
@@ -106,7 +106,7 @@ export class WebSocketAuthorizer extends Resource implements IWebSocketAuthorize
106106
super(scope, id);
107107

108108
if (props.type === WebSocketAuthorizerType.LAMBDA && !props.authorizerUri) {
109-
throw new Error('authorizerUri is mandatory for Lambda authorizers');
109+
throw new ValidationError('authorizerUri is mandatory for Lambda authorizers', scope);
110110
}
111111

112112
const resource = new CfnAuthorizer(this, 'Resource', {

packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IWebSocketRoute } from './route';
44
import { CfnIntegration } from '.././index';
55
import { IRole } from '../../../aws-iam';
66
import { Duration, Resource } from '../../../core';
7+
import { ValidationError } from '../../../core/lib/errors';
78
import { IIntegration } from '../common';
89

910
/**
@@ -172,7 +173,7 @@ export class WebSocketIntegration extends Resource implements IWebSocketIntegrat
172173
super(scope, id);
173174

174175
if (props.timeout && !props.timeout.isUnresolved() && (props.timeout.toMilliseconds() < 50 || props.timeout.toMilliseconds() > 29000)) {
175-
throw new Error('Integration timeout must be between 50 milliseconds and 29 seconds.');
176+
throw new ValidationError('Integration timeout must be between 50 milliseconds and 29 seconds.', scope);
176177
}
177178

178179
const integ = new CfnIntegration(this, 'Resource', {
@@ -228,7 +229,7 @@ export abstract class WebSocketRouteIntegration {
228229
*/
229230
public _bindToRoute(options: WebSocketRouteIntegrationBindOptions): { readonly integrationId: string } {
230231
if (this.integration && this.integration.webSocketApi.node.addr !== options.route.webSocketApi.node.addr) {
231-
throw new Error('A single integration cannot be associated with multiple APIs.');
232+
throw new ValidationError('A single integration cannot be associated with multiple APIs.', options.scope);
232233
}
233234

234235
if (!this.integration) {

packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IWebSocketRouteAuthorizer, WebSocketNoneAuthorizer } from './authorizer
44
import { WebSocketRouteIntegration } from './integration';
55
import { CfnRoute, CfnRouteResponse } from '.././index';
66
import { Resource } from '../../../core';
7+
import { ValidationError } from '../../../core/lib/errors';
78
import { IRoute } from '../common';
89

910
/**
@@ -85,7 +86,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute {
8586
super(scope, id);
8687

8788
if (props.routeKey != '$connect' && props.authorizer) {
88-
throw new Error('You can only set a WebSocket authorizer to a $connect route.');
89+
throw new ValidationError('You can only set a WebSocket authorizer to a $connect route.', scope);
8990
}
9091

9192
this.webSocketApi = props.webSocketApi;

packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/stage.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IWebSocketApi } from './api';
33
import { CfnStage } from '.././index';
44
import { Grant, IGrantable } from '../../../aws-iam';
55
import { Stack } from '../../../core';
6+
import { ValidationError } from '../../../core/lib/errors';
67
import { StageOptions, IApi, IStage, StageAttributes } from '../common';
78
import { StageBase } from '../common/base';
89

@@ -64,11 +65,11 @@ export class WebSocketStage extends StageBase implements IWebSocketStage {
6465
public readonly api = attrs.api;
6566

6667
get url(): string {
67-
throw new Error('url is not available for imported stages.');
68+
throw new ValidationError('url is not available for imported stages.', scope);
6869
}
6970

7071
get callbackUrl(): string {
71-
throw new Error('callback url is not available for imported stages.');
72+
throw new ValidationError('callback url is not available for imported stages.', scope);
7273
}
7374
}
7475
return new Import(scope, id);

0 commit comments

Comments
 (0)