Skip to content

Commit 7a4ddc5

Browse files
fix(web): bound the readiness route by its own timeout
Two related issues surfaced from bot review on the readiness route: 1. `checkZoekt` called `loadZoektClient()` outside the `withTimeout` wrapper, so a stalled first-call Zoekt init (vendored proto load, network DNS, etc.) could exceed the documented 2s bound. Move the init call inside the timeout so it shares the same bound as the gRPC call. 2. `withTimeout` raced the check promise against the timeout and discarded the loser. If the loser later rejected, no one was awaiting it, surfacing as an unhandled-promise-rejection warning in the Node process during a hung-dependency outage. Attach a no-op `.catch` to the check promise so late rejections are absorbed; the visible result (the timeout error or the actual check error) is unchanged. Addresses Bugbot findings 597dbe01 and 367ae57f, and CodeRabbit finding 'Cancel timed-out readiness dependency operations' on the same file.
1 parent 2a47d11 commit 7a4ddc5

1 file changed

Lines changed: 12 additions & 5 deletions

File tree

  • packages/web/src/app/api/(server)/health/ready

‎packages/web/src/app/api/(server)/health/ready/route.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,27 @@ type ReadinessResponse = {
2626
};
2727
};
2828

29-
// Wraps a check function in a per-check timeout. When the timeout fires
30-
// first, the check resolves as an error result; the underlying promise is
31-
// allowed to settle in the background (its result is discarded).
29+
// Runs `check()` and rejects if it has not settled after `timeoutMs`. When the
30+
// timeout fires, the underlying check promise may still resolve or reject
31+
// later; the no-op `.catch` below attaches to that promise so a late
32+
// rejection does not surface as an unhandled-promise-rejection in the Node
33+
// process while the readiness request has already moved on.
3234
const withTimeout = async <T>(
3335
label: string,
3436
check: () => Promise<T>,
3537
timeoutMs: number,
3638
): Promise<T> => {
39+
const checkPromise = check();
40+
checkPromise.catch(() => { /* swallowed: see comment above */ });
41+
3742
let timer: ReturnType<typeof setTimeout> | undefined;
3843
const timeout = new Promise<never>((_, reject) => {
3944
timer = setTimeout(() => {
4045
reject(new Error(`${label} check timed out after ${timeoutMs}ms`));
4146
}, timeoutMs);
4247
});
4348
try {
44-
return await Promise.race([check(), timeout]);
49+
return await Promise.race([checkPromise, timeout]);
4550
} finally {
4651
if (timer) {
4752
clearTimeout(timer);
@@ -88,8 +93,10 @@ const checkRedis = async (): Promise<CheckResult> => {
8893
const checkZoekt = async (): Promise<CheckResult> => {
8994
const start = Date.now();
9095
try {
91-
const client = await loadZoektClient();
9296
await withTimeout('zoekt', async () => {
97+
// Build the client inside the timeout so a first-call init stall
98+
// (e.g., vendored proto load) is also bounded.
99+
const client = await loadZoektClient();
93100
await new Promise<void>((resolve, reject) => {
94101
// An empty List with a 1s wall-time cap is the smallest request
95102
// that exercises the gRPC channel end-to-end. It returns an

0 commit comments

Comments
 (0)