From 8feedcb832d731ea461c79ca1b25463f4c7110d5 Mon Sep 17 00:00:00 2001 From: Martin-Zack Mekkaoui Date: Tue, 28 Jul 2026 15:05:15 +0200 Subject: [PATCH] fix(cloudfront-origins): readTimeout and keepaliveTimeout reject valid values Both props are validated against a hardcoded upper bound of 180 seconds, which matches no current CloudFront quota: - Response timeout per origin is documented as 1-120 seconds, adjustable via a limit increase. The 180 bound is a stale snapshot (it was 60, #18628 raised it to 180, and the documented default has since moved to 120), so values a raised quota permits are rejected at synth time with no way to express them through the L2. - Keep-alive timeout per origin is documented as 1-300 seconds. Here the bound is simply wrong today: values between 181 and 300 need no quota increase at all, yet synth refuses them. The existing doc comment already states that values above the default require an approved limit increase and otherwise fail at deploy time, so the construct already defers enforcement of these quotas to the service. Hardcoding a ceiling contradicts that, and re-hardcoding a higher number would only recur at the next quota change. Remove the upper bound for both props in HttpOrigin, VpcOrigin, FunctionUrlOrigin and RestApiOrigin. The 1-second minimum and the whole-number-of-seconds validation are unchanged, and validateSecondsInRangeOrUndefined now accepts an optional max so other callers keep their bounds. Doc comments now describe each prop's own quota rather than citing the response timeout quota for both. --- .../lib/function-url-origin.ts | 18 ++++++----- .../aws-cloudfront-origins/lib/http-origin.ts | 14 ++++---- .../lib/private/utils.ts | 11 +++++-- .../lib/rest-api-origin.ts | 14 ++++---- .../aws-cloudfront-origins/lib/vpc-origin.ts | 14 ++++---- .../test/http-origin.test.ts | 30 +++++++++++++---- .../test/vpc-origin.test.ts | 32 ++++++++++++++++--- 7 files changed, 94 insertions(+), 39 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/function-url-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/function-url-origin.ts index 70c9b6a8997f2..b2d48da162667 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/function-url-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/function-url-origin.ts @@ -12,7 +12,8 @@ import { lit } from '../../core/lib/private/literal-string'; export interface FunctionUrlOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront waits for a response from the origin. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the origin response timeout quota, which is + * adjustable, so the effective maximum depends on the target account. * * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. @@ -23,10 +24,11 @@ export interface FunctionUrlOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront persists its connection to the origin. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the keep-alive timeout per origin quota, + * which is adjustable, so the effective maximum depends on the target account. * - * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota - * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. + * The default quota allows up to 300 seconds; higher values require an approved limit increase + * in the target account, and otherwise produce an error at deploy time. * * @default Duration.seconds(5) */ @@ -77,8 +79,8 @@ export class FunctionUrlOrigin extends cloudfront.OriginBase { const domainName = cdk.Fn.select(2, cdk.Fn.split('/', lambdaFunctionUrl.url)); super(domainName, props); - validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, undefined, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, undefined, props.keepaliveTimeout); this.validateResponseCompletionTimeoutWithReadTimeout(props.responseCompletionTimeout, props.readTimeout); } @@ -109,8 +111,8 @@ class FunctionUrlOriginWithOAC extends cloudfront.OriginBase { this.props = props; - validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, undefined, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, undefined, props.keepaliveTimeout); } protected renderCustomOriginConfig(): cloudfront.CfnDistribution.CustomOriginConfigProperty | undefined { diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/http-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/http-origin.ts index d0a42ef5c056f..05ced39c93cc6 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/http-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/http-origin.ts @@ -38,7 +38,8 @@ export interface HttpOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront waits for a response from the origin, also known as the origin response timeout. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the origin response timeout quota, which is + * adjustable, so the effective maximum depends on the target account. * * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. @@ -49,10 +50,11 @@ export interface HttpOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront persists its connection to the origin. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the keep-alive timeout per origin quota, + * which is adjustable, so the effective maximum depends on the target account. * - * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota - * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. + * The default quota allows up to 300 seconds; higher values require an approved limit increase + * in the target account, and otherwise produce an error at deploy time. * * @default Duration.seconds(5) */ @@ -75,8 +77,8 @@ export class HttpOrigin extends cloudfront.OriginBase { constructor(domainName: string, private readonly props: HttpOriginProps = {}) { super(domainName, props); - validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, undefined, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, undefined, props.keepaliveTimeout); this.validateResponseCompletionTimeoutWithReadTimeout(props.responseCompletionTimeout, props.readTimeout); this.validatePortNumber('httpPort', props.httpPort); diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/private/utils.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/private/utils.ts index edf4aaa85fac1..18031472d9e99 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/private/utils.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/private/utils.ts @@ -3,11 +3,16 @@ import { lit } from '../../../core/lib/private/literal-string'; /** * Throws an error if a duration is defined and not an integer number of seconds within a range. + * + * Pass `undefined` for `max` to validate the lower bound only. This is for values whose upper + * bound is an adjustable service quota, so the effective maximum depends on the target account + * and can only be enforced by the service at deploy time. */ -export function validateSecondsInRangeOrUndefined(name: string, min: number, max: number, duration?: cdk.Duration) { +export function validateSecondsInRangeOrUndefined(name: string, min: number, max: number | undefined, duration?: cdk.Duration) { if (duration === undefined) { return; } const value = duration.toSeconds(); - if (!Number.isInteger(value) || value < min || value > max) { - throw new cdk.UnscopedValidationError(lit`InvalidDurationRange`, `${name}: Must be an int between ${min} and ${max} seconds (inclusive); received ${value}.`); + if (!Number.isInteger(value) || value < min || (max !== undefined && value > max)) { + const range = max !== undefined ? `between ${min} and ${max} seconds (inclusive)` : `${min} seconds or greater`; + throw new cdk.UnscopedValidationError(lit`InvalidDurationRange`, `${name}: Must be an int ${range}; received ${value}.`); } } diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/rest-api-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/rest-api-origin.ts index 03518a532d685..a5160f621841c 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/rest-api-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/rest-api-origin.ts @@ -9,7 +9,8 @@ import * as cdk from '../../core'; export interface RestApiOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront waits for a response from the origin, also known as the origin response timeout. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the origin response timeout quota, which is + * adjustable, so the effective maximum depends on the target account. * * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. @@ -20,10 +21,11 @@ export interface RestApiOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront persists its connection to the origin. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the keep-alive timeout per origin quota, + * which is adjustable, so the effective maximum depends on the target account. * - * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota - * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. + * The default quota allows up to 300 seconds; higher values require an approved limit increase + * in the target account, and otherwise produce an error at deploy time. * * @default Duration.seconds(5) */ @@ -43,8 +45,8 @@ export class RestApiOrigin extends cloudfront.OriginBase { ...props, }); - validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, undefined, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, undefined, props.keepaliveTimeout); this.validateResponseCompletionTimeoutWithReadTimeout(props.responseCompletionTimeout, props.readTimeout); } diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/vpc-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/vpc-origin.ts index 55acf4fd40773..a6b501dc38b2f 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/vpc-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/vpc-origin.ts @@ -18,7 +18,8 @@ export interface VpcOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront waits for a response from the origin, also known as the origin response timeout. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the origin response timeout quota, which is + * adjustable, so the effective maximum depends on the target account. * * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. @@ -29,10 +30,11 @@ export interface VpcOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront persists its connection to the origin. - * The valid range is from 1 to 180 seconds, inclusive. + * The minimum is 1 second. The maximum is governed by the keep-alive timeout per origin quota, + * which is adjustable, so the effective maximum depends on the target account. * - * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota - * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. + * The default quota allows up to 300 seconds; higher values require an approved limit increase + * in the target account, and otherwise produce an error at deploy time. * * @default Duration.seconds(5) */ @@ -83,8 +85,8 @@ export abstract class VpcOrigin extends cloudfront.OriginBase { protected constructor(domainName: string, protected readonly props: VpcOriginProps) { super(domainName, props); - validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, undefined, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, undefined, props.keepaliveTimeout); } protected renderVpcOriginConfig(): cloudfront.CfnDistribution.VpcOriginConfigProperty | undefined { diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/test/http-origin.test.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/test/http-origin.test.ts index 373835feec0d7..7bf0cb12953bc 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/test/http-origin.test.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/test/http-origin.test.ts @@ -71,20 +71,29 @@ test('renders an example with all available props', () => { test.each([ Duration.seconds(0), +])('validates readTimeout is at least 1 second', (readTimeout) => { + expect(() => { + new HttpOrigin('www.example.com', { + readTimeout, + }); + }).toThrow(`readTimeout: Must be an int 1 seconds or greater; received ${readTimeout.toSeconds()}.`); +}); + +test.each([ Duration.seconds(181), Duration.minutes(5), -])('validates readTimeout is an integer between 1 and 180 seconds - out of bounds', (readTimeout) => { +])('allows readTimeout above the default quota, which the service validates at deploy time', (readTimeout) => { expect(() => { new HttpOrigin('www.example.com', { readTimeout, }); - }).toThrow(`readTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${readTimeout.toSeconds()}.`); + }).not.toThrow(); }); test.each([ Duration.seconds(0.5), Duration.seconds(60.5), -])('validates readTimeout is an integer between 1 and 180 seconds - not an int', (readTimeout) => { +])('validates readTimeout is a whole number of seconds', (readTimeout) => { expect(() => { new HttpOrigin('www.example.com', { readTimeout, @@ -94,20 +103,29 @@ test.each([ test.each([ Duration.seconds(0), +])('validates keepaliveTimeout is at least 1 second', (keepaliveTimeout) => { + expect(() => { + new HttpOrigin('www.example.com', { + keepaliveTimeout, + }); + }).toThrow(`keepaliveTimeout: Must be an int 1 seconds or greater; received ${keepaliveTimeout.toSeconds()}.`); +}); + +test.each([ Duration.seconds(181), Duration.minutes(5), -])('validates keepaliveTimeout is an integer between 1 and 180 seconds - out of bounds', (keepaliveTimeout) => { +])('allows keepaliveTimeout within the default quota, which the service validates at deploy time', (keepaliveTimeout) => { expect(() => { new HttpOrigin('www.example.com', { keepaliveTimeout, }); - }).toThrow(`keepaliveTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${keepaliveTimeout.toSeconds()}.`); + }).not.toThrow(); }); test.each([ Duration.seconds(0.5), Duration.seconds(60.5), -])('validates keepaliveTimeout is an integer between 1 and 180 seconds - not an int', (keepaliveTimeout) => { +])('validates keepaliveTimeout is a whole number of seconds', (keepaliveTimeout) => { expect(() => { new HttpOrigin('www.example.com', { keepaliveTimeout, diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/test/vpc-origin.test.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/test/vpc-origin.test.ts index a62d18bb6ac81..be43f0042295a 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/test/vpc-origin.test.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/test/vpc-origin.test.ts @@ -206,9 +206,21 @@ test('VPC origin from an imported VpcOrigin resource', () => { test.each([ Duration.seconds(0), +])('VPC origin throws when readTimeout is %s - below the minimum', (readTimeout) => { + // GIVEN + const vpc = new ec2.Vpc(stack, 'Vpc'); + const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc }); + + // WHEN + expect(() => { + VpcOrigin.withApplicationLoadBalancer(loadBalancer, { readTimeout }); + }).toThrow(`readTimeout: Must be an int 1 seconds or greater; received ${readTimeout.toSeconds()}`); +}); + +test.each([ Duration.seconds(181), Duration.minutes(5), -])('VPC origin throws when readTimeout is %s - out of bounds', (readTimeout) => { +])('VPC origin allows readTimeout of %s above the default quota', (readTimeout) => { // GIVEN const vpc = new ec2.Vpc(stack, 'Vpc'); const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc }); @@ -216,14 +228,26 @@ test.each([ // WHEN expect(() => { VpcOrigin.withApplicationLoadBalancer(loadBalancer, { readTimeout }); - }).toThrow(`readTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${readTimeout.toSeconds()}`); + }).not.toThrow(); }); test.each([ Duration.seconds(0), +])('VPC origin throws when keepaliveTimeout is %s - below the minimum', (keepaliveTimeout) => { + // GIVEN + const vpc = new ec2.Vpc(stack, 'Vpc'); + const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc }); + + // WHEN + expect(() => { + VpcOrigin.withApplicationLoadBalancer(loadBalancer, { keepaliveTimeout }); + }).toThrow(`keepaliveTimeout: Must be an int 1 seconds or greater; received ${keepaliveTimeout.toSeconds()}`); +}); + +test.each([ Duration.seconds(181), Duration.minutes(5), -])('VPC origin throws when keepaliveTimeout is %s - out of bounds', (keepaliveTimeout) => { +])('VPC origin allows keepaliveTimeout of %s within the default quota', (keepaliveTimeout) => { // GIVEN const vpc = new ec2.Vpc(stack, 'Vpc'); const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc }); @@ -231,7 +255,7 @@ test.each([ // WHEN expect(() => { VpcOrigin.withApplicationLoadBalancer(loadBalancer, { keepaliveTimeout }); - }).toThrow(`keepaliveTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${keepaliveTimeout.toSeconds()}`); + }).not.toThrow(); }); test('VPC origin throws when no domainName is specified', () => {