Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions .changeset/profiling-api-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"cf": patch
---

Allow long-running API operations to finish

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.
46 changes: 44 additions & 2 deletions packages/cli/src/__tests__/lib/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((resolve) => {
startRequest = resolve;
});
const client = createCloudflareClientWithToken({
apiToken: "test-token",
fetch: async (_url, init) => {
startRequest();
return new Promise<Response>((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 = () => {
Expand All @@ -190,7 +232,7 @@ describe("createCommandClient", () => {
return new Promise<Response>((resolve, reject) => {
const timer = setTimeout(
() => resolve(new Response(null, { status: 204 })),
31_000
61_000
);
init?.signal?.addEventListener(
"abort",
Expand All @@ -212,7 +254,7 @@ describe("createCommandClient", () => {
{ body: Buffer.from("data") }
);
await started;
await vi.advanceTimersByTimeAsync(31_000);
await vi.advanceTimersByTimeAsync(61_000);
await expect(request).resolves.toBeNull();
} finally {
vi.useRealTimers();
Expand Down
35 changes: 32 additions & 3 deletions packages/cli/src/__tests__/lib/raw-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(60_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 60-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(60_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;
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/api-constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/** Default timeout for API calls (30 seconds). */
export const API_TIMEOUT_MS = 30_000;
/** Default timeout for API calls (60 seconds). */
export const API_TIMEOUT_MS = 60_000;
2 changes: 1 addition & 1 deletion packages/cli/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading