From dd655ef6dabae6066698b66b9c66ed00ca49b4d2 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Sat, 10 Oct 2026 12:13:36 -0500 Subject: [PATCH 1/2] fix: allow long-running API operations to finish --- .changeset/profiling-api-timeout.md | 10 ++++ packages/cli/src/__tests__/lib/auth.test.ts | 46 ++++++++++++++++++- .../cli/src/__tests__/lib/raw-fetch.test.ts | 35 ++++++++++++-- packages/cli/src/lib/api-constants.ts | 4 +- packages/cli/src/lib/auth.ts | 2 +- 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 .changeset/profiling-api-timeout.md diff --git a/.changeset/profiling-api-timeout.md b/.changeset/profiling-api-timeout.md new file mode 100644 index 000000000..df3257f0c --- /dev/null +++ b/.changeset/profiling-api-timeout.md @@ -0,0 +1,10 @@ +--- +"cf": patch +--- + +Allow long-running API operations to finish + +Increase the default API request timeout from 30 to 90 seconds so Workers +profiling captures can run for the full supported 50 seconds and return their +results. Apply the same default to SDK and raw-response requests while retaining +explicit timeout overrides and the exemption for binary and multipart uploads. diff --git a/packages/cli/src/__tests__/lib/auth.test.ts b/packages/cli/src/__tests__/lib/auth.test.ts index 97f790932..a308f987e 100644 --- a/packages/cli/src/__tests__/lib/auth.test.ts +++ b/packages/cli/src/__tests__/lib/auth.test.ts @@ -174,6 +174,48 @@ describe("createCommandClient", () => { ).rejects.toThrow("Request timed out after 10ms"); }); + it("lets JSON requests finish after a 50-second operation", async () => { + vi.useFakeTimers(); + let startRequest: () => void = () => { + throw new Error("Request did not start"); + }; + const started = new Promise((resolve) => { + startRequest = resolve; + }); + const client = createCloudflareClientWithToken({ + apiToken: "test-token", + fetch: async (_url, init) => { + startRequest(); + return new Promise((resolve, reject) => { + const timer = setTimeout( + () => resolve(new Response(null, { status: 204 })), + 50_000 + ); + init?.signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + reject(init.signal?.reason); + }, + { once: true } + ); + }); + }, + }); + + try { + const request = requestApi(client, "POST", "/capture", { + body: { duration_ms: 50_000 }, + }); + const result = expect(request).resolves.toBeNull(); + await started; + await vi.advanceTimersByTimeAsync(50_000); + await result; + } finally { + vi.useRealTimers(); + } + }); + it("lets binary uploads continue past the standard API deadline", async () => { vi.useFakeTimers(); let startRequest: () => void = () => { @@ -190,7 +232,7 @@ describe("createCommandClient", () => { return new Promise((resolve, reject) => { const timer = setTimeout( () => resolve(new Response(null, { status: 204 })), - 31_000 + 91_000 ); init?.signal?.addEventListener( "abort", @@ -212,7 +254,7 @@ describe("createCommandClient", () => { { body: Buffer.from("data") } ); await started; - await vi.advanceTimersByTimeAsync(31_000); + await vi.advanceTimersByTimeAsync(91_000); await expect(request).resolves.toBeNull(); } finally { vi.useRealTimers(); diff --git a/packages/cli/src/__tests__/lib/raw-fetch.test.ts b/packages/cli/src/__tests__/lib/raw-fetch.test.ts index 9e157d742..c3bc6648d 100644 --- a/packages/cli/src/__tests__/lib/raw-fetch.test.ts +++ b/packages/cli/src/__tests__/lib/raw-fetch.test.ts @@ -97,18 +97,47 @@ describe("fetchRawBytes", () => { it("applies the standard API timeout", async () => { process.env.CLOUDFLARE_API_TOKEN = "prod-token"; + const timeout = vi.spyOn(AbortSignal, "timeout"); let signal: AbortSignal | undefined; globalThis.fetch = (async (_input, init) => { signal = init?.signal ?? undefined; return new Response("", { status: 200 }); }) as typeof globalThis.fetch; - await fetchRawBytes("/accounts/abc/anything"); + try { + await fetchRawBytes("/accounts/abc/anything"); - expect(signal).toBeInstanceOf(AbortSignal); - expect(signal?.aborted).toBe(false); + expect(timeout).toHaveBeenCalledWith(90_000); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal?.aborted).toBe(false); + } finally { + timeout.mockRestore(); + } }); + it.each([{ duration_ms: 50_000 }, JSON.stringify({ duration_ms: 50_000 })])( + "gives JSON profile captures a 90-second deadline (%j)", + async (body) => { + process.env.CLOUDFLARE_API_TOKEN = "prod-token"; + const timeout = vi.spyOn(AbortSignal, "timeout"); + const bytes = Buffer.from([0, 255, 128, 65]); + globalThis.fetch = async () => new Response(bytes); + + try { + await expect( + fetchRawBytes("/accounts/abc/capture", { + method: "POST", + body, + contentType: "application/json", + }) + ).resolves.toEqual(bytes); + expect(timeout).toHaveBeenCalledWith(90_000); + } finally { + timeout.mockRestore(); + } + } + ); + it("omits the request deadline for binary uploads", async () => { process.env.CLOUDFLARE_API_TOKEN = "prod-token"; let signal: AbortSignal | null | undefined; diff --git a/packages/cli/src/lib/api-constants.ts b/packages/cli/src/lib/api-constants.ts index 76a73a121..39748f9f3 100644 --- a/packages/cli/src/lib/api-constants.ts +++ b/packages/cli/src/lib/api-constants.ts @@ -1,2 +1,2 @@ -/** Default timeout for API calls (30 seconds). */ -export const API_TIMEOUT_MS = 30_000; +/** Default timeout for API calls (90 seconds). */ +export const API_TIMEOUT_MS = 90_000; diff --git a/packages/cli/src/lib/auth.ts b/packages/cli/src/lib/auth.ts index eac77da4f..0f3efc1ef 100644 --- a/packages/cli/src/lib/auth.ts +++ b/packages/cli/src/lib/auth.ts @@ -262,7 +262,7 @@ export function createCloudflareClientWithToken( clientTimeouts.set(client, options.timeout ?? API_TIMEOUT_MS); if (options.timeout === undefined) { // SDK passthrough requests inherit the client's timeout. Use a separate - // client for uploads so typed API calls retain their 30-second deadline. + // client for uploads so typed API calls retain their standard deadline. uploadClients.set( client, new CloudflareApiClient({ From 56481a79e4e4ac1f9f3996ffb4056f321bf1a9d7 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Sat, 10 Oct 2026 12:23:24 -0500 Subject: [PATCH 2/2] fix: use a 60-second default API timeout --- .changeset/profiling-api-timeout.md | 2 +- packages/cli/src/__tests__/lib/auth.test.ts | 4 ++-- packages/cli/src/__tests__/lib/raw-fetch.test.ts | 6 +++--- packages/cli/src/lib/api-constants.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.changeset/profiling-api-timeout.md b/.changeset/profiling-api-timeout.md index df3257f0c..f09958bd6 100644 --- a/.changeset/profiling-api-timeout.md +++ b/.changeset/profiling-api-timeout.md @@ -4,7 +4,7 @@ Allow long-running API operations to finish -Increase the default API request timeout from 30 to 90 seconds so Workers +Increase the default API request timeout from 30 to 60 seconds so Workers profiling captures can run for the full supported 50 seconds and return their results. Apply the same default to SDK and raw-response requests while retaining explicit timeout overrides and the exemption for binary and multipart uploads. diff --git a/packages/cli/src/__tests__/lib/auth.test.ts b/packages/cli/src/__tests__/lib/auth.test.ts index a308f987e..b356951e3 100644 --- a/packages/cli/src/__tests__/lib/auth.test.ts +++ b/packages/cli/src/__tests__/lib/auth.test.ts @@ -232,7 +232,7 @@ describe("createCommandClient", () => { return new Promise((resolve, reject) => { const timer = setTimeout( () => resolve(new Response(null, { status: 204 })), - 91_000 + 61_000 ); init?.signal?.addEventListener( "abort", @@ -254,7 +254,7 @@ describe("createCommandClient", () => { { body: Buffer.from("data") } ); await started; - await vi.advanceTimersByTimeAsync(91_000); + await vi.advanceTimersByTimeAsync(61_000); await expect(request).resolves.toBeNull(); } finally { vi.useRealTimers(); diff --git a/packages/cli/src/__tests__/lib/raw-fetch.test.ts b/packages/cli/src/__tests__/lib/raw-fetch.test.ts index c3bc6648d..552366983 100644 --- a/packages/cli/src/__tests__/lib/raw-fetch.test.ts +++ b/packages/cli/src/__tests__/lib/raw-fetch.test.ts @@ -107,7 +107,7 @@ describe("fetchRawBytes", () => { try { await fetchRawBytes("/accounts/abc/anything"); - expect(timeout).toHaveBeenCalledWith(90_000); + expect(timeout).toHaveBeenCalledWith(60_000); expect(signal).toBeInstanceOf(AbortSignal); expect(signal?.aborted).toBe(false); } finally { @@ -116,7 +116,7 @@ describe("fetchRawBytes", () => { }); it.each([{ duration_ms: 50_000 }, JSON.stringify({ duration_ms: 50_000 })])( - "gives JSON profile captures a 90-second deadline (%j)", + "gives JSON profile captures a 60-second deadline (%j)", async (body) => { process.env.CLOUDFLARE_API_TOKEN = "prod-token"; const timeout = vi.spyOn(AbortSignal, "timeout"); @@ -131,7 +131,7 @@ describe("fetchRawBytes", () => { contentType: "application/json", }) ).resolves.toEqual(bytes); - expect(timeout).toHaveBeenCalledWith(90_000); + expect(timeout).toHaveBeenCalledWith(60_000); } finally { timeout.mockRestore(); } diff --git a/packages/cli/src/lib/api-constants.ts b/packages/cli/src/lib/api-constants.ts index 39748f9f3..c300f63b4 100644 --- a/packages/cli/src/lib/api-constants.ts +++ b/packages/cli/src/lib/api-constants.ts @@ -1,2 +1,2 @@ -/** Default timeout for API calls (90 seconds). */ -export const API_TIMEOUT_MS = 90_000; +/** Default timeout for API calls (60 seconds). */ +export const API_TIMEOUT_MS = 60_000;