Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apigatewayv2): throw ValidationError instead of untyped errors #33072

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const enableNoThrowDefaultErrorIn = [
'aws-ssmcontacts',
'aws-ssmincidents',
'aws-ssmquicksetup',
'aws-apigatewayv2',
];
baseConfig.overrides.push({
files: enableNoThrowDefaultErrorIn.map(m => `./${m}/lib/**`),
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 @@ -315,7 +315,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 @@ -364,7 +364,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 @@ -417,7 +417,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 @@ -477,8 +477,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 @@ -487,7 +486,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 @@ -117,7 +118,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 @@ -201,7 +202,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
Loading