From f10eae4803920e62b364a6d40199ddcc7665cb77 Mon Sep 17 00:00:00 2001 From: Ahmed Hamed Date: Tue, 28 Jul 2026 13:53:51 +0200 Subject: [PATCH 1/4] fix(custom-resources): retry CloudFormation response PUT in AwsCustomResource handler The AwsCustomResource handler's respond() sent the CloudFormation response with a single, un-retried https PUT to the pre-signed S3 response URL and passed `resolve` directly as the response callback, so the HTTP status code was never inspected. A transient PUT failure or a non-2xx response was silently swallowed, so CloudFormation never received the response and waited out its ~1 hour timeout even though the function logged SUCCESS and exited cleanly. Extract the retry + exponential-backoff and status-code-checking HTTP logic (matching the provider framework runtime) into a shared module, lib/shared/http-response.ts, and use it from respond(): retry on network errors and >= 400 responses, treating only a successful response as success. sim: CFN-118294 --- .../aws-custom-resource-handler/utils.ts | 27 +++--- .../lib/shared/http-response.ts | 77 +++++++++++++++++ .../aws-custom-resource-handler/utils.test.ts | 86 ++++++++++++++++++- .../aws-sdk-v3-handler.test.ts | 6 +- .../test/shared/http-response.test.ts | 85 ++++++++++++++++++ 5 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts index 091ad7c46957c..be5f6d429ec89 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts @@ -2,6 +2,8 @@ import type { AwsCredentialIdentityProvider } from '@smithy/types'; import type { AwsSdkCall } from './construct-types'; +import { httpRequest, withRetries } from '../../shared/http-response'; +import type { RetryOptions } from '../../shared/http-response'; type Event = AWSLambda.CloudFormationCustomResourceEvent; @@ -10,6 +12,20 @@ type Event = AWSLambda.CloudFormationCustomResourceEvent; */ export const PHYSICAL_RESOURCE_ID_REFERENCE = 'PHYSICAL:RESOURCEID:'; +/** + * Retry options used when sending the response to CloudFormation. + * + * The response PUT to the pre-signed S3 response URL is retried with + * exponential backoff on a network error or a non-successful (>= 400) HTTP + * response, instead of being sent exactly once and silently swallowed. Without + * this, a single transient PUT failure causes CloudFormation to wait out its + * ~1 hour timeout even though the function already logged a SUCCESS response. + */ +const RESPONSE_RETRY_OPTIONS: RetryOptions = { + attempts: 5, + sleep: 1000, +}; + /** * Decodes encoded special values (physicalResourceId) */ @@ -82,16 +98,7 @@ export function respond( }, }; - return new Promise((resolve, reject) => { - try { - const request = require('https').request(requestOptions, resolve); - request.on('error', reject); - request.write(responseBody); - request.end(); - } catch (e) { - reject(e); - } - }); + return withRetries(RESPONSE_RETRY_OPTIONS, httpRequest)(requestOptions, responseBody); } /** diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts b/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts new file mode 100644 index 0000000000000..3388dd30e0398 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts @@ -0,0 +1,77 @@ +import * as https from 'https'; + +/** + * Shared helpers for sending a Custom Resource response back to CloudFormation + * over the pre-signed S3 response URL. + * + * This mirrors the behavior of the custom resource provider framework runtime + * (`aws-cdk-lib/custom-resources/.../provider-framework/runtime`): the response + * PUT is retried with exponential backoff, and only a successful (< 400) HTTP + * response is treated as success. It lives here so it can be shared by the + * bundled handlers in this package instead of being reimplemented per handler. + * + * NOTE: this module can only be consumed by handlers that are minified and + * bundled by the custom-resources-framework (`minifyAndBundle: true`), because + * esbuild inlines the import. Handlers that are copied verbatim + * (`minifyAndBundle: false`, e.g. the nodejs-entrypoint handler) cannot import + * it and must keep their own self-contained copy. + */ + +export interface RetryOptions { + /** How many retries (will at least try once) */ + readonly attempts: number; + /** Sleep base, in ms */ + readonly sleep: number; +} + +/** + * Wraps an async function so it is retried with exponential backoff (and + * jitter) on failure, throwing the last error once the attempts are exhausted. + */ +export function withRetries, B>(options: RetryOptions, fn: (...xs: A) => Promise): (...xs: A) => Promise { + return async (...xs: A) => { + let attempts = options.attempts; + let ms = options.sleep; + while (true) { + try { + return await fn(...xs); + } catch (e) { + if (attempts-- <= 0) { + throw e; + } + await sleep(Math.floor(Math.random() * ms)); + ms *= 2; + } + } + }; +} + +async function sleep(ms: number): Promise { + return new Promise((ok) => setTimeout(ok, ms)); +} + +/** + * Performs a single HTTP request (used to PUT the response to the CloudFormation + * pre-signed S3 response URL). Rejects on a network error or a non-successful + * (>= 400) HTTP response so that a caller wrapping it in `withRetries` can retry, + * instead of treating any received response as success. + */ +export async function httpRequest(options: https.RequestOptions, requestBody: string): Promise { + return new Promise((resolve, reject) => { + try { + const request = https.request(options, (response) => { + response.resume(); // Consume the response but don't care about it + if (!response.statusCode || response.statusCode >= 400) { + reject(new Error(`Unsuccessful HTTP response: ${response.statusCode}`)); + } else { + resolve(); + } + }); + request.on('error', reject); + request.write(requestBody); + request.end(); + } catch (e) { + reject(e); + } + }); +} diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource-handler/utils.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource-handler/utils.test.ts index 3fe35a9395ea3..ed6764a855248 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource-handler/utils.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource-handler/utils.test.ts @@ -1,5 +1,8 @@ +import * as https from 'https'; import type { AwsSdkCall } from '../../../lib/custom-resources/aws-custom-resource-handler/construct-types'; -import { getCredentials } from '../../../lib/custom-resources/aws-custom-resource-handler/utils'; +import { getCredentials, respond } from '../../../lib/custom-resources/aws-custom-resource-handler/utils'; + +jest.mock('https'); // Mock the @aws-sdk/credential-providers import const mockFromTemporaryCredentials = jest.fn(); @@ -184,3 +187,84 @@ describe('getCredentials with External ID support', () => { expect(result).toBe(mockCredentials); }); }); + +describe('respond', () => { + function makeEvent(): AWSLambda.CloudFormationCustomResourceEvent { + return { + ResponseURL: 'https://cfn.example.com/response?token=abc', + StackId: '', + RequestId: '', + LogicalResourceId: '', + ResourceType: '', + ServiceToken: '', + ResourceProperties: { ServiceToken: '' }, + RequestType: 'Create', + } as any; + } + + beforeEach(() => { + // make backoff sleeps instantaneous and deterministic + jest.spyOn(Math, 'random').mockReturnValue(0); + jest.spyOn(console, 'log').mockImplementation(() => undefined); + (https.request as jest.Mock).mockReset(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('PUTs the response to the CloudFormation response URL on success', async () => { + // GIVEN + let capturedOptions: any; + let capturedBody: string | undefined; + (https.request as jest.Mock).mockImplementation((options: any, cb: any) => { + capturedOptions = options; + cb({ statusCode: 200, resume: jest.fn() }); + return { on: jest.fn(), write: (b: string) => { capturedBody = b; }, end: jest.fn() }; + }); + + // WHEN + await respond(makeEvent(), 'SUCCESS', 'reason', 'physical-id', { Foo: 'Bar' }, true); + + // THEN + expect(https.request).toHaveBeenCalledTimes(1); + expect(capturedOptions.method).toEqual('PUT'); + expect(capturedOptions.hostname).toEqual('cfn.example.com'); + expect(capturedOptions.path).toEqual('/response?token=abc'); + const parsedBody = JSON.parse(capturedBody!); + expect(parsedBody.Status).toEqual('SUCCESS'); + expect(parsedBody.PhysicalResourceId).toEqual('physical-id'); + expect(parsedBody.Data).toEqual({ Foo: 'Bar' }); + }); + + test('retries a transient failure and then succeeds', async () => { + // GIVEN: first attempt returns a 500, second attempt returns a 200 + (https.request as jest.Mock) + .mockImplementationOnce((_options: any, cb: any) => { + cb({ statusCode: 500, resume: jest.fn() }); + return { on: jest.fn(), write: jest.fn(), end: jest.fn() }; + }) + .mockImplementationOnce((_options: any, cb: any) => { + cb({ statusCode: 200, resume: jest.fn() }); + return { on: jest.fn(), write: jest.fn(), end: jest.fn() }; + }); + + // WHEN / THEN + await expect(respond(makeEvent(), 'SUCCESS', 'reason', 'physical-id', {}, false)).resolves.toBeUndefined(); + expect(https.request).toHaveBeenCalledTimes(2); + }); + + test('rejects after exhausting retries when every attempt fails', async () => { + // GIVEN: every attempt returns a 500 + (https.request as jest.Mock).mockImplementation((_options: any, cb: any) => { + cb({ statusCode: 500, resume: jest.fn() }); + return { on: jest.fn(), write: jest.fn(), end: jest.fn() }; + }); + + // WHEN / THEN + // RESPONSE_RETRY_OPTIONS.attempts is 5 => 1 initial call + 5 retries = 6 invocations + await expect(respond(makeEvent(), 'SUCCESS', 'reason', 'physical-id', {}, false)) + .rejects.toThrow('Unsuccessful HTTP response: 500'); + expect(https.request).toHaveBeenCalledTimes(6); + }); +}); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource/aws-sdk-v3-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource/aws-sdk-v3-handler.test.ts index 2959d4b5d9d3b..3fcd00d940d01 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource/aws-sdk-v3-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources/aws-custom-resource/aws-sdk-v3-handler.test.ts @@ -66,11 +66,13 @@ jest.mock('@aws-sdk/credential-providers', () => { jest.mock('https', () => { return { ...jest.requireActual('https'), - request: (_: any, callback: () => void) => { + request: (_: any, callback: (res: any) => void) => { return { on: () => undefined, write: () => true, - end: callback, + // Invoke the response callback with a successful response so the + // handler's status-code check (>= 400 => retry/reject) passes. + end: () => callback({ statusCode: 200, resume: () => undefined }), }; }, }; diff --git a/packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts new file mode 100644 index 0000000000000..a8b4fc666211f --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts @@ -0,0 +1,85 @@ +import * as https from 'https'; +import { httpRequest, withRetries } from '../../lib/shared/http-response'; + +jest.mock('https'); + +describe('withRetries', () => { + beforeEach(() => { + // make backoff sleeps instantaneous and deterministic + jest.spyOn(Math, 'random').mockReturnValue(0); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('returns the result without retrying when the function succeeds', async () => { + const fn = jest.fn().mockResolvedValue('ok'); + + const result = await withRetries({ attempts: 5, sleep: 1 }, fn)(); + + expect(result).toEqual('ok'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + test('retries and eventually succeeds', async () => { + const fn = jest.fn() + .mockRejectedValueOnce(new Error('transient')) + .mockRejectedValueOnce(new Error('transient')) + .mockResolvedValue('ok'); + + const result = await withRetries({ attempts: 5, sleep: 1 }, fn)(); + + expect(result).toEqual('ok'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test('throws the last error after exhausting all attempts', async () => { + const fn = jest.fn().mockRejectedValue(new Error('always fails')); + + // attempts: 3 => 1 initial call + 3 retries = 4 invocations + await expect(withRetries({ attempts: 3, sleep: 1 }, fn)()).rejects.toThrow('always fails'); + expect(fn).toHaveBeenCalledTimes(4); + }); +}); + +describe('httpRequest', () => { + afterEach(() => { + jest.restoreAllMocks(); + (https.request as jest.Mock).mockReset(); + }); + + test('resolves on a successful (< 400) response', async () => { + (https.request as jest.Mock).mockImplementation((_options: any, cb: any) => { + cb({ statusCode: 200, resume: jest.fn() }); + return { on: jest.fn(), write: jest.fn(), end: jest.fn() }; + }); + + await expect(httpRequest({}, 'body')).resolves.toBeUndefined(); + }); + + test('rejects on a >= 400 response', async () => { + (https.request as jest.Mock).mockImplementation((_options: any, cb: any) => { + cb({ statusCode: 500, resume: jest.fn() }); + return { on: jest.fn(), write: jest.fn(), end: jest.fn() }; + }); + + await expect(httpRequest({}, 'body')).rejects.toThrow('Unsuccessful HTTP response: 500'); + }); + + test('rejects on a network error', async () => { + (https.request as jest.Mock).mockImplementation((_options: any, _cb: any) => { + return { + on: (event: string, handler: (e: Error) => void) => { + if (event === 'error') { + handler(new Error('socket hang up')); + } + }, + write: jest.fn(), + end: jest.fn(), + }; + }); + + await expect(httpRequest({}, 'body')).rejects.toThrow('socket hang up'); + }); +}); From 8392b86bedba476991cd24aabd67249d2bc830e7 Mon Sep 17 00:00:00 2001 From: Ahmed Hamed Date: Tue, 28 Jul 2026 16:27:57 +0200 Subject: [PATCH 2/4] fix(custom-resources): use the shared response-retry logic in the log-retention handler too The log-retention handler's respond() had the same single, un-retried https PUT to the CloudFormation pre-signed S3 response URL as the AwsCustomResource handler, with no HTTP status-code check. Point it at the shared lib/shared/http-response.ts (withRetries + httpRequest) so both bundled handlers retry the response PUT consistently, and hoist the shared default retry options (DEFAULT_RESPONSE_RETRY_OPTIONS) into that module. The nodejs-entrypoint handler intentionally keeps its own copy: it is packaged with minifyAndBundle: false (copied verbatim, because it dynamically requires the user handler), so it cannot import a relative shared module. sim: CFN-118294 --- .../aws-logs/log-retention-handler/index.ts | 13 ++----------- .../aws-custom-resource-handler/utils.ts | 19 ++----------------- .../lib/shared/http-response.ts | 12 ++++++++++++ 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts index 7d1d406484efa..435fbbc184b84 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import * as Logs from '@aws-sdk/client-cloudwatch-logs'; +import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../shared/http-response'; let FAKE_SLEEP = false; @@ -166,17 +167,7 @@ export async function handler(event: LogRetentionEvent, context: AWSLambda.Conte }, }; - return new Promise((resolve, reject) => { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const request = require('https').request(requestOptions, resolve); - request.on('error', reject); - request.write(responseBody); - request.end(); - } catch (e) { - reject(e); - } - }); + return withRetries(DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest)(requestOptions, responseBody); } } diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts index be5f6d429ec89..9e971c2b8663a 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts @@ -2,8 +2,7 @@ import type { AwsCredentialIdentityProvider } from '@smithy/types'; import type { AwsSdkCall } from './construct-types'; -import { httpRequest, withRetries } from '../../shared/http-response'; -import type { RetryOptions } from '../../shared/http-response'; +import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../shared/http-response'; type Event = AWSLambda.CloudFormationCustomResourceEvent; @@ -12,20 +11,6 @@ type Event = AWSLambda.CloudFormationCustomResourceEvent; */ export const PHYSICAL_RESOURCE_ID_REFERENCE = 'PHYSICAL:RESOURCEID:'; -/** - * Retry options used when sending the response to CloudFormation. - * - * The response PUT to the pre-signed S3 response URL is retried with - * exponential backoff on a network error or a non-successful (>= 400) HTTP - * response, instead of being sent exactly once and silently swallowed. Without - * this, a single transient PUT failure causes CloudFormation to wait out its - * ~1 hour timeout even though the function already logged a SUCCESS response. - */ -const RESPONSE_RETRY_OPTIONS: RetryOptions = { - attempts: 5, - sleep: 1000, -}; - /** * Decodes encoded special values (physicalResourceId) */ @@ -98,7 +83,7 @@ export function respond( }, }; - return withRetries(RESPONSE_RETRY_OPTIONS, httpRequest)(requestOptions, responseBody); + return withRetries(DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest)(requestOptions, responseBody); } /** diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts b/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts index 3388dd30e0398..4e78068e1244f 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts @@ -24,6 +24,18 @@ export interface RetryOptions { readonly sleep: number; } +/** + * Default retry options for sending a Custom Resource response to CloudFormation. + * + * Shared by the bundled handlers so they retry the response PUT consistently + * (5 attempts, exponential backoff from a 1s base) instead of each PUT being + * a single un-retried attempt. + */ +export const DEFAULT_RESPONSE_RETRY_OPTIONS: RetryOptions = { + attempts: 5, + sleep: 1000, +}; + /** * Wraps an async function so it is retried with exponential backoff (and * jitter) on failure, throwing the last error once the attempts are exhausted. From 2a307930a28ff2cc93b8ae445e3f14b89382c7d9 Mon Sep 17 00:00:00 2001 From: Ahmed Hamed Date: Tue, 28 Jul 2026 16:48:08 +0200 Subject: [PATCH 3/4] refactor(custom-resources): rename shared response module to lib/utils.ts Move lib/shared/http-response.ts to lib/utils.ts (and the test to test/utils.test.ts), and update the aws-custom-resource and log-retention handlers to import from '../../utils'. No behavior change. sim: CFN-118294 --- .../lib/aws-logs/log-retention-handler/index.ts | 2 +- .../lib/custom-resources/aws-custom-resource-handler/utils.ts | 2 +- .../lib/{shared/http-response.ts => utils.ts} | 0 .../test/{shared/http-response.test.ts => utils.test.ts} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/@aws-cdk/custom-resource-handlers/lib/{shared/http-response.ts => utils.ts} (100%) rename packages/@aws-cdk/custom-resource-handlers/test/{shared/http-response.test.ts => utils.test.ts} (97%) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts index 435fbbc184b84..a589407d14174 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-logs/log-retention-handler/index.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import * as Logs from '@aws-sdk/client-cloudwatch-logs'; -import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../shared/http-response'; +import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../utils'; let FAKE_SLEEP = false; diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts index 9e971c2b8663a..b291c922f38e3 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources/aws-custom-resource-handler/utils.ts @@ -2,7 +2,7 @@ import type { AwsCredentialIdentityProvider } from '@smithy/types'; import type { AwsSdkCall } from './construct-types'; -import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../shared/http-response'; +import { DEFAULT_RESPONSE_RETRY_OPTIONS, httpRequest, withRetries } from '../../utils'; type Event = AWSLambda.CloudFormationCustomResourceEvent; diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts b/packages/@aws-cdk/custom-resource-handlers/lib/utils.ts similarity index 100% rename from packages/@aws-cdk/custom-resource-handlers/lib/shared/http-response.ts rename to packages/@aws-cdk/custom-resource-handlers/lib/utils.ts diff --git a/packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/utils.test.ts similarity index 97% rename from packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts rename to packages/@aws-cdk/custom-resource-handlers/test/utils.test.ts index a8b4fc666211f..cd3834ff80f76 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/shared/http-response.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/utils.test.ts @@ -1,5 +1,5 @@ import * as https from 'https'; -import { httpRequest, withRetries } from '../../lib/shared/http-response'; +import { httpRequest, withRetries } from '../lib/utils'; jest.mock('https'); From 56593c27599fbeb1d2d750a4a21a24bd1e6bba87 Mon Sep 17 00:00:00 2001 From: Ahmed Hamed Date: Tue, 28 Jul 2026 16:59:02 +0200 Subject: [PATCH 4/4] docs(custom-resources): link provider-framework source permalinks in utils.ts jsdoc sim: CFN-118294 --- .../custom-resource-handlers/lib/utils.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/utils.ts b/packages/@aws-cdk/custom-resource-handlers/lib/utils.ts index 4e78068e1244f..8f4b5a8581974 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/utils.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/utils.ts @@ -4,11 +4,20 @@ import * as https from 'https'; * Shared helpers for sending a Custom Resource response back to CloudFormation * over the pre-signed S3 response URL. * - * This mirrors the behavior of the custom resource provider framework runtime - * (`aws-cdk-lib/custom-resources/.../provider-framework/runtime`): the response - * PUT is retried with exponential backoff, and only a successful (< 400) HTTP - * response is treated as success. It lives here so it can be shared by the - * bundled handlers in this package instead of being reimplemented per handler. + * This mirrors the behavior of the custom resource provider framework runtime: + * the response PUT is retried with exponential backoff, and only a successful + * (< 400) HTTP response is treated as success. It lives here so it can be + * shared by the bundled handlers in this package instead of being + * reimplemented per handler. + * + * `withRetries` mirrors: + * https://github.com/aws/aws-cdk/blob/3b1df7422f1e922849e94ec2a90928e6f2a05163/packages/aws-cdk-lib/custom-resources/lib/provider-framework/runtime/util.ts#L24-L40 + * `httpRequest` mirrors `defaultHttpRequest` from: + * https://github.com/aws/aws-cdk/blob/3b1df7422f1e922849e94ec2a90928e6f2a05163/packages/aws-cdk-lib/custom-resources/lib/provider-framework/runtime/outbound.ts#L19-L36 + * + * These cannot be imported directly: `aws-cdk-lib` depends on this package, so + * importing from it would create a circular dependency (see the + * `copied-from-aws-cdk-lib/` directory, which copies code for the same reason). * * NOTE: this module can only be consumed by handlers that are minified and * bundled by the custom-resources-framework (`minifyAndBundle: true`), because