Skip to content

Commit 3ec4fce

Browse files
committed
fix(feishu): pass timeoutMs through app-registration guarded fetch
1 parent 242cdca commit 3ec4fce

2 files changed

Lines changed: 48 additions & 6 deletions

File tree

extensions/feishu/src/app-registration.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
77
import { afterEach, describe, expect, it, vi } from "vitest";
88
import {
99
beginAppRegistration,
10+
FEISHU_APP_REGISTRATION_TIMEOUT_MS,
1011
type FeishuAppRegistrationFetch,
1112
pollAppRegistration,
1213
printQrCode,
@@ -254,6 +255,40 @@ describe("Feishu app registration", () => {
254255
expect(writeSpy).toHaveBeenCalledWith("terminal-qr\n");
255256
});
256257

258+
it("times out registration POSTs when accounts never return headers", async () => {
259+
expect(FEISHU_APP_REGISTRATION_TIMEOUT_MS).toBe(10_000);
260+
await withRegistrationServer(
261+
// Accept TCP but never write headers — idle body timers never start.
262+
(_req, _res) => {},
263+
async (options) => {
264+
const started = Date.now();
265+
const outcome = await beginAppRegistration("feishu", {
266+
...options,
267+
// Stand-in for FEISHU_APP_REGISTRATION_TIMEOUT_MS; keep the suite fast.
268+
timeoutMs: 80,
269+
}).then(
270+
(value) => ({ ok: true as const, value }),
271+
(error: unknown) => ({ ok: false as const, error }),
272+
);
273+
const elapsedMs = Date.now() - started;
274+
expect(outcome.ok).toBe(false);
275+
if (!outcome.ok) {
276+
expect(outcome.error).toMatchObject({
277+
name: "TimeoutError",
278+
message: "request timed out",
279+
});
280+
}
281+
expect(elapsedMs).toBeGreaterThanOrEqual(60);
282+
expect(elapsedMs).toBeLessThan(2_000);
283+
console.log(
284+
`[feishu fetchFeishuJson hang proof] timed_out=${!outcome.ok} name=${
285+
outcome.ok ? "n/a" : (outcome.error as Error).name
286+
} elapsed_ms=${elapsedMs} production_timeout_ms=${FEISHU_APP_REGISTRATION_TIMEOUT_MS}`,
287+
);
288+
},
289+
);
290+
});
291+
257292
// over-cap: body > 16 MiB, no Content-Length. The bounded reader cancels
258293
// through the real SSRF guard and rejects before full buffering.
259294
it("rejects Feishu API responses that exceed the 16 MiB JSON body cap", async () => {

extensions/feishu/src/app-registration.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const LARK_ACCOUNTS_URL = "https://accounts.larksuite.com";
2222

2323
const REGISTRATION_PATH = "/oauth/v1/app/registration";
2424

25-
const REQUEST_TIMEOUT_MS = 10_000;
25+
/** Wizard/onboarding hang floor for registration guarded fetches (not FEISHU_HTTP_TIMEOUT_MS). */
26+
export const FEISHU_APP_REGISTRATION_TIMEOUT_MS = 10_000;
2627
const DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS = 5;
2728
const DEFAULT_REGISTRATION_EXPIRE_SECONDS = 600;
2829

@@ -57,6 +58,8 @@ type FeishuAppRegistrationFetchOptions = {
5758
fetchImpl?: FeishuAppRegistrationFetch;
5859
/** Override hostname lookup for hermetic SSRF-guard tests. */
5960
lookupFn?: LookupFn;
61+
/** Override the guarded-fetch hang floor (defaults to FEISHU_APP_REGISTRATION_TIMEOUT_MS). */
62+
timeoutMs?: number;
6063
};
6164

6265
interface RawBeginResponse {
@@ -105,11 +108,11 @@ async function postRegistration<T>(
105108
method: "POST",
106109
headers: { "Content-Type": "application/x-www-form-urlencoded" },
107110
body: new URLSearchParams(body).toString(),
108-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
109111
},
110112
auditContext: "feishu.app-registration.post",
111113
fetchImpl: options?.fetchImpl,
112114
lookupFn: options?.lookupFn,
115+
timeoutMs: options?.timeoutMs,
113116
});
114117
}
115118

@@ -119,12 +122,18 @@ async function fetchFeishuJson<T>(params: {
119122
auditContext: string;
120123
fetchImpl?: FeishuAppRegistrationFetch;
121124
lookupFn?: LookupFn;
125+
timeoutMs?: number;
122126
}): Promise<T> {
127+
// Mirror streaming-card: pass timeoutMs into fetchWithSsrFGuard so undici
128+
// dispatcher + guarded abort both fire. Registration keeps the tighter
129+
// FEISHU_APP_REGISTRATION_TIMEOUT_MS wizard budget (not FEISHU_HTTP_TIMEOUT_MS).
130+
const timeoutMs = params.timeoutMs ?? FEISHU_APP_REGISTRATION_TIMEOUT_MS;
123131
const { response, release } = await fetchWithSsrFGuard({
124132
url: params.url,
125133
init: params.init,
126134
fetchImpl: params.fetchImpl,
127135
lookupFn: params.lookupFn,
136+
timeoutMs,
128137
policy: { allowedHostnames: [new URL(params.url).hostname] },
129138
auditContext: params.auditContext,
130139
});
@@ -229,7 +238,7 @@ export async function pollAppRegistration(params: {
229238
const expireInMs =
230239
finiteSecondsToTimerSafeMilliseconds(expireIn) ??
231240
finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_EXPIRE_SECONDS) ??
232-
REQUEST_TIMEOUT_MS;
241+
FEISHU_APP_REGISTRATION_TIMEOUT_MS;
233242
const deadline = Date.now() + expireInMs;
234243

235244
while (Date.now() < deadline) {
@@ -342,7 +351,6 @@ export async function getAppOwnerOpenId(params: {
342351
method: "POST",
343352
headers: { "Content-Type": "application/json" },
344353
body: JSON.stringify({ app_id: params.appId, app_secret: params.appSecret }),
345-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
346354
},
347355
auditContext: "feishu.app-registration.owner-token",
348356
fetchImpl: params.fetchImpl,
@@ -369,7 +377,6 @@ export async function getAppOwnerOpenId(params: {
369377
Authorization: `Bearer ${tokenData.tenant_access_token}`,
370378
"Content-Type": "application/json",
371379
},
372-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
373380
},
374381
auditContext: "feishu.app-registration.owner-app",
375382
fetchImpl: params.fetchImpl,
@@ -395,6 +402,6 @@ function sleepRegistrationPollInterval(intervalSeconds: number): Promise<void> {
395402
const intervalMs =
396403
finiteSecondsToTimerSafeMilliseconds(intervalSeconds) ??
397404
finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS) ??
398-
REQUEST_TIMEOUT_MS;
405+
FEISHU_APP_REGISTRATION_TIMEOUT_MS;
399406
return sleep(intervalMs);
400407
}

0 commit comments

Comments
 (0)