From 18e107f494f1121096ad32fd0eb8bc59cd423dd1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 30 Jul 2026 10:03:39 +0800 Subject: [PATCH] fix(plugins): report request timeouts for stalled response bodies (#116166) * fix(openai): preserve device-code request timeouts * fix(msteams): preserve SharePoint upload timeouts * fix(qqbot): preserve channel API request timeouts --- extensions/msteams/src/graph-upload.test.ts | 4 +--- extensions/msteams/src/graph-upload.ts | 5 +++-- extensions/openai/openai-chatgpt-device-code.test.ts | 9 ++++----- extensions/openai/openai-chatgpt-device-code.ts | 5 +++-- extensions/qqbot/src/engine/tools/channel-api.ts | 8 ++++++-- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index 141fc3a68fe7..d66201f3a6dd 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -287,9 +287,7 @@ describe("graph upload request timeouts", () => { const signal = fetchSignal(fetchFn); const assertion = expectMSTeamsTimeout(upload, "MS Teams SharePoint upload", timeoutMs); - await vi.advanceTimersByTimeAsync(timeoutMs); - - await assertion; + await Promise.all([assertion, vi.advanceTimersByTimeAsync(timeoutMs)]); expect(signal.aborted).toBe(true); }); diff --git a/extensions/msteams/src/graph-upload.ts b/extensions/msteams/src/graph-upload.ts index 022d9f840ec0..63367eaf993b 100644 --- a/extensions/msteams/src/graph-upload.ts +++ b/extensions/msteams/src/graph-upload.ts @@ -75,10 +75,11 @@ async function uploadToSharePoint(params: { // Use "OpenClawShared" folder to organize bot-uploaded files const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`; + const timeoutMs = resolveMSTeamsSharePointUploadTimeoutMs(params.buffer.length); const data = await withMSTeamsAbortableRequestTimeout({ label: SHAREPOINT_UPLOAD_TIMEOUT_LABEL, - timeoutMs: resolveMSTeamsSharePointUploadTimeoutMs(params.buffer.length), + timeoutMs, work: async (signal) => { const token = await getGraphAccessToken(params.tokenProvider); const res = await fetchFn( @@ -103,7 +104,7 @@ async function uploadToSharePoint(params: { id?: string; webUrl?: string; name?: string; - }>(res, "msteams.graph-upload.uploadSharePointFile"); + }>(res, "msteams.graph-upload.uploadSharePointFile", { chunkTimeoutMs: timeoutMs }); }, }); diff --git a/extensions/openai/openai-chatgpt-device-code.test.ts b/extensions/openai/openai-chatgpt-device-code.test.ts index daad1e98f26d..4d70f9e25329 100644 --- a/extensions/openai/openai-chatgpt-device-code.test.ts +++ b/extensions/openai/openai-chatgpt-device-code.test.ts @@ -123,11 +123,10 @@ describe("loginOpenAICodexDeviceCode", () => { await vi.advanceTimersByTimeAsync(0); expect(fetchMock).toHaveBeenCalledOnce(); - const rejected = expect(login).rejects.toThrow( - "OpenAI device code user code request timed out after 30000ms", - ); - await vi.advanceTimersByTimeAsync(30_000); - await rejected; + await Promise.all([ + expect(login).rejects.toThrow("OpenAI device code user code request timed out after 30000ms"), + vi.advanceTimersByTimeAsync(30_000), + ]); }); it("still honors caller cancellation during an active device-code request", async () => { diff --git a/extensions/openai/openai-chatgpt-device-code.ts b/extensions/openai/openai-chatgpt-device-code.ts index b5d314c02010..ac80fc56bcd6 100644 --- a/extensions/openai/openai-chatgpt-device-code.ts +++ b/extensions/openai/openai-chatgpt-device-code.ts @@ -151,12 +151,13 @@ function formatDeviceCodeError(params: { : `${params.prefix}: HTTP ${params.status}`; } -async function readOpenAICodexDeviceBody(response: Response): Promise { +async function readOpenAICodexDeviceBody(response: Response, timeoutMs: number): Promise { return await readResponseTextLimited( response, response.ok ? OPENAI_CODEX_DEVICE_JSON_BODY_LIMIT_BYTES : OPENAI_CODEX_DEVICE_ERROR_BODY_LIMIT_BYTES, + { chunkTimeoutMs: timeoutMs }, ); } @@ -185,7 +186,7 @@ async function runOpenAICodexDeviceRequest(params: { return { ok: response.ok, status: response.status, - bodyText: await readOpenAICodexDeviceBody(response), + bodyText: await readOpenAICodexDeviceBody(response, params.timeoutMs), }; } finally { await release(); diff --git a/extensions/qqbot/src/engine/tools/channel-api.ts b/extensions/qqbot/src/engine/tools/channel-api.ts index ba08f92bf491..9d6683ef6938 100644 --- a/extensions/qqbot/src/engine/tools/channel-api.ts +++ b/extensions/qqbot/src/engine/tools/channel-api.ts @@ -339,8 +339,12 @@ export async function executeChannelApi( debugLog(`[qqbot-channel-api] <<< Status: ${res.status} ${res.statusText}`); const rawBody = res.ok - ? await readProviderTextResponse(res, "QQ channel API response") - : await readResponseTextLimited(res, CHANNEL_API_ERROR_BODY_LIMIT_BYTES); + ? await readProviderTextResponse(res, "QQ channel API response", { + chunkTimeoutMs: DEFAULT_TIMEOUT_MS, + }) + : await readResponseTextLimited(res, CHANNEL_API_ERROR_BODY_LIMIT_BYTES, { + chunkTimeoutMs: DEFAULT_TIMEOUT_MS, + }); if (!rawBody || rawBody.trim() === "") { if (res.ok) { return json({ success: true, status: res.status, path: params.path });