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
This commit is contained in:
Vincent Koc
2026-07-30 10:03:39 +08:00
committed by GitHub
parent 5a3c3b3f4f
commit 18e107f494
5 changed files with 17 additions and 14 deletions

View File

@@ -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);
});

View File

@@ -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 });
},
});

View File

@@ -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 () => {

View File

@@ -151,12 +151,13 @@ function formatDeviceCodeError(params: {
: `${params.prefix}: HTTP ${params.status}`;
}
async function readOpenAICodexDeviceBody(response: Response): Promise<string> {
async function readOpenAICodexDeviceBody(response: Response, timeoutMs: number): Promise<string> {
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();

View File

@@ -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 });