Skip to content

Commit

Permalink
feat(apigatewayv2): throw ValidationError instead of untyped errors (
Browse files Browse the repository at this point in the history
…#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*
  • Loading branch information
HBobertz authored Jan 24, 2025
1 parent cc1988a commit 8b472fc
Show file tree
Hide file tree
Showing 14 changed files with 55 additions and 36 deletions.
10 changes: 10 additions & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ const enableNoThrowDefaultErrorIn = [
'aws-elasticloadbalancingv2-targets',
'aws-lambda',
'aws-rds',
'aws-s3',
'aws-sns',
'aws-sqs',
'aws-ssm',
'aws-ssmcontacts',
'aws-ssmincidents',
'aws-ssmquicksetup',
'aws-apigatewayv2',
'aws-apigatewayv2-authorizers',
'aws-synthetics',
'aws-route53',
'aws-route53-patterns',
'aws-route53-targets',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IDomainName } from './domain-name';
import { IStage } from './stage';
import { CfnApiMapping, CfnApiMappingProps } from '.././index';
import { IResource, Resource } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';

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

if (props.apiMappingKey === '') {
throw new Error('empty string for api mapping key not allowed');
throw new ValidationError('empty string for api mapping key not allowed', scope);
}

const apiMappingProps: CfnApiMappingProps = {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ApiMapping } from './api-mapping';
import { DomainMappingOptions, IStage } from './stage';
import * as cloudwatch from '../../../aws-cloudwatch';
import { Resource } from '../../../core';
import { UnscopedValidationError } from '../../../core/lib/errors';

/**
* Base class representing an API
Expand Down Expand Up @@ -46,7 +47,7 @@ export abstract class StageBase extends Resource implements IStage {
*/
protected _addDomainMapping(domainMapping: DomainMappingOptions) {
if (this._apiMapping) {
throw new Error('Only one ApiMapping allowed per Stage');
throw new UnscopedValidationError('Only one ApiMapping allowed per Stage');
}
this._apiMapping = new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, {
api: this.baseApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CfnDomainName, CfnDomainNameProps } from '.././index';
import { ICertificate } from '../../../aws-certificatemanager';
import { IBucket } from '../../../aws-s3';
import { IResource, Lazy, Resource, Token } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';

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

if (props.domainName === '') {
throw new Error('empty string for domainName not allowed');
throw new ValidationError('empty string for domainName not allowed', scope);
}

// validation for ownership certificate
if (props.ownershipCertificate && !props.mtls) {
throw new Error('ownership certificate can only be used with mtls domains');
throw new ValidationError('ownership certificate can only be used with mtls domains', scope);
}

const mtlsConfig = this.configureMTLS(props.mtls);
Expand Down Expand Up @@ -225,7 +226,7 @@ export class DomainName extends Resource implements IDomainName {
private validateEndpointType(endpointType: string | undefined) : void {
for (let config of this.domainNameConfigurations) {
if (endpointType && endpointType == config.endpointType) {
throw new Error(`an endpoint with type ${endpointType} already exists`);
throw new ValidationError(`an endpoint with type ${endpointType} already exists`, this);
}
}
}
Expand Down
13 changes: 6 additions & 7 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/http/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { VpcLink, VpcLinkProps } from './vpc-link';
import { CfnApi, CfnApiProps } from '.././index';
import { Metric, MetricOptions } from '../../../aws-cloudwatch';
import { ArnFormat, Duration, Stack, Token } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { IApi } from '../common/api';
import { ApiBase } from '../common/base';
import { DomainMappingOptions } from '../common/stage';

/**
* Represents an HTTP API
*/
Expand Down Expand Up @@ -314,7 +314,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th

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

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

public get apiEndpoint(): string {
if (!this._apiEndpoint) {
throw new Error('apiEndpoint is not configured on the imported HttpApi.');
throw new ValidationError('apiEndpoint is not configured on the imported HttpApi.', scope);
}
return this._apiEndpoint;
}
Expand Down Expand Up @@ -416,7 +416,7 @@ export class HttpApi extends HttpApiBase {
if (props?.corsPreflight) {
const cors = props.corsPreflight;
if (cors.allowOrigins && cors.allowOrigins.includes('*') && cors.allowCredentials) {
throw new Error("CORS preflight - allowCredentials is not supported when allowOrigin is '*'");
throw new ValidationError("CORS preflight - allowCredentials is not supported when allowOrigin is '*'", scope);
}
const {
allowCredentials,
Expand Down Expand Up @@ -476,8 +476,7 @@ export class HttpApi extends HttpApiBase {
}

if (props?.createDefaultStage === false && props.defaultDomainMapping) {
throw new Error('defaultDomainMapping not supported with createDefaultStage disabled',
);
throw new ValidationError('defaultDomainMapping not supported with createDefaultStage disabled', scope);
}
}

Expand All @@ -486,7 +485,7 @@ export class HttpApi extends HttpApiBase {
*/
public get apiEndpoint(): string {
if (this.disableExecuteApiEndpoint) {
throw new Error('apiEndpoint is not accessible when disableExecuteApiEndpoint is set to true.');
throw new ValidationError('apiEndpoint is not accessible when disableExecuteApiEndpoint is set to true.', this);
}
return this._apiEndpoint;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/http/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IHttpApi } from './api';
import { IHttpRoute } from './route';
import { CfnAuthorizer } from '.././index';
import { Duration, Resource } from '../../../core';

import { ValidationError } from '../../../core/lib/errors';
import { IAuthorizer } from '../common';

/**
Expand Down Expand Up @@ -161,11 +161,11 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer {
let authorizerPayloadFormatVersion = props.payloadFormatVersion;

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

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

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/http/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HttpMethod, IHttpRoute } from './route';
import { CfnIntegration } from '.././index';
import { IRole } from '../../../aws-iam';
import { Aws, Duration, Resource } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { IIntegration } from '../common';
import { ParameterMapping } from '../parameter-mapping';

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

if (!props.integrationSubtype && !props.integrationUri) {
throw new Error('Either `integrationSubtype` or `integrationUri` must be specified.');
throw new ValidationError('Either `integrationSubtype` or `integrationUri` must be specified.', scope);
}

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

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

if (!this.integration) {
Expand Down
11 changes: 6 additions & 5 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/http/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HttpRouteIntegration } from './integration';
import { CfnRoute, CfnRouteProps } from '.././index';
import * as iam from '../../../aws-iam';
import { Aws, Resource } from '../../../core';
import { UnscopedValidationError, ValidationError } from '../../../core/lib/errors';
import { IRoute } from '../common';

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

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

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

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

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

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

const resourceArns = httpMethods.map(httpMethod => {
Expand Down
7 changes: 4 additions & 3 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IHttpApi } from './api';
import { CfnStage } from '.././index';
import { Metric, MetricOptions } from '../../../aws-cloudwatch';
import { Stack } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { StageOptions, IStage, StageAttributes } from '../common';
import { IApi } from '../common/api';
import { StageBase } from '../common/base';
Expand Down Expand Up @@ -144,11 +145,11 @@ export class HttpStage extends HttpStageBase {
public readonly api = attrs.api;

get url(): string {
throw new Error('url is not available for imported stages.');
throw new ValidationError('url is not available for imported stages.', scope);
}

get domainUrl(): string {
throw new Error('domainUrl is not available for imported stages.');
throw new ValidationError('domainUrl is not available for imported stages.', scope);
}
}
return new Import(scope, id);
Expand Down Expand Up @@ -194,7 +195,7 @@ export class HttpStage extends HttpStageBase {

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

return `https://${this._apiMapping.domainName.name}/${this._apiMapping.mappingKey ?? ''}`;
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WebSocketRoute, WebSocketRouteOptions } from './route';
import { CfnApi } from '.././index';
import { Grant, IGrantable } from '../../../aws-iam';
import { ArnFormat, Stack, Token } from '../../../core';
import { UnscopedValidationError, ValidationError } from '../../../core/lib/errors';
import { IApi } from '../common/api';
import { ApiBase } from '../common/base';

Expand Down Expand Up @@ -116,7 +117,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {

public get apiEndpoint(): string {
if (!this._apiEndpoint) {
throw new Error('apiEndpoint is not configured on the imported WebSocketApi.');
throw new ValidationError('apiEndpoint is not configured on the imported WebSocketApi.', scope);
}
return this._apiEndpoint;
}
Expand Down Expand Up @@ -200,7 +201,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {
*/
public arnForExecuteApi(method?: string, path?: string, stage?: string): string {
if (path && !Token.isUnresolved(path) && !path.startsWith('/')) {
throw new Error(`Path must start with '/': ${path}`);
throw new UnscopedValidationError(`Path must start with '/': ${path}`);
}

if (method && method.toUpperCase() === 'ANY') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IWebSocketApi } from './api';
import { IWebSocketRoute } from './route';
import { CfnAuthorizer } from '.././index';
import { Resource } from '../../../core';

import { ValidationError } from '../../../core/lib/errors';
import { IAuthorizer } from '../common';

/**
Expand Down Expand Up @@ -106,7 +106,7 @@ export class WebSocketAuthorizer extends Resource implements IWebSocketAuthorize
super(scope, id);

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

const resource = new CfnAuthorizer(this, 'Resource', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IWebSocketRoute } from './route';
import { CfnIntegration } from '.././index';
import { IRole } from '../../../aws-iam';
import { Duration, Resource } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { IIntegration } from '../common';

/**
Expand Down Expand Up @@ -172,7 +173,7 @@ export class WebSocketIntegration extends Resource implements IWebSocketIntegrat
super(scope, id);

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

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

if (!this.integration) {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IWebSocketRouteAuthorizer, WebSocketNoneAuthorizer } from './authorizer
import { WebSocketRouteIntegration } from './integration';
import { CfnRoute, CfnRouteResponse } from '.././index';
import { Resource } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { IRoute } from '../common';

/**
Expand Down Expand Up @@ -85,7 +86,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute {
super(scope, id);

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

this.webSocketApi = props.webSocketApi;
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IWebSocketApi } from './api';
import { CfnStage } from '.././index';
import { Grant, IGrantable } from '../../../aws-iam';
import { Stack } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { StageOptions, IApi, IStage, StageAttributes } from '../common';
import { StageBase } from '../common/base';

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

get url(): string {
throw new Error('url is not available for imported stages.');
throw new ValidationError('url is not available for imported stages.', scope);
}

get callbackUrl(): string {
throw new Error('callback url is not available for imported stages.');
throw new ValidationError('callback url is not available for imported stages.', scope);
}
}
return new Import(scope, id);
Expand Down

0 comments on commit 8b472fc

Please sign in to comment.