Skip to content

Commit c169019

Browse files
committed
e2e: bound per-attempt fetch timeout in spawn waitForReady
The CI flake we kept hitting (opencode serve binds 0.0.0.0:PORT, prints 'server listening', completes SQLite migration, then the loopback HTTP probe times out at 300s) was a Bun fetch issue, not an opencode issue. Bun has a hardcoded ~5 minute fetch timeout that ignores AbortSignal.timeout values longer than the limit (oven-sh/bun#16682). The probe's fetch() calls had no AbortSignal, so a single hung request could hold the loop for the entire ~5 minute window — exhausting the overall 300s deadline with curl-fallback diagnostics never firing (curlEverSucceeded=false, curlLastErr=null in error output). Fix: bound every fetch attempt at 2s via AbortSignal.timeout so no single request can starve the deadline. Removed the curl-fallback path entirely since the root cause was fetch hanging, not fetch being fundamentally unable to reach the server.
1 parent 7eb88c3 commit c169019

1 file changed

Lines changed: 18 additions & 40 deletions

File tree

  • packages/e2e-tests/src/opencode-runner

packages/e2e-tests/src/opencode-runner/spawn.ts

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ function writeConfigs(
172172
* Wait until the opencode server responds to GET /doc (an endpoint that exists in
173173
* OpenCode's server). Polls for up to `timeoutMs`.
174174
*
175-
* Diagnostic design:
176-
* - Primary probe is Bun's `fetch()` (what production SDK clients use).
177-
* - Every 30s of consecutive fetch failures we fall back to `curl --max-time 2`
178-
* as a sanity check. If curl succeeds where fetch keeps failing, the issue
179-
* is Bun's HTTP client (not opencode), and we proceed treating the server
180-
* as ready — logging a one-line diagnostic so failures are attributable.
181-
* - If both fetch and curl fail for the full deadline, we throw with the
182-
* captured probe state so the error message is actionable.
175+
* Implementation note — Bun fetch timeout flake:
176+
* Bun's default `fetch()` has a hardcoded ~5 minute timeout that ignores
177+
* AbortSignal.timeout values longer than the limit
178+
* (https://github.com/oven-sh/bun/issues/16682). If we don't bound each
179+
* fetch attempt explicitly, a single hung request can hold the loop for
180+
* the entire ~5 minute window, blowing past our overall deadline before
181+
* we get any chance to retry. Pass a short AbortSignal.timeout on every
182+
* attempt so one bad fetch can't starve the deadline.
183183
*/
184184
// Default bumped from 30s → 300s. GitHub-hosted runners can take much longer
185185
// than 30s for `opencode serve` to bind its port + finish plugin init + complete
@@ -189,53 +189,31 @@ function writeConfigs(
189189
// genuine readiness failures — 5 minutes is still far above any realistic boot.
190190
async function waitForReady(url: string, timeoutMs = 300_000): Promise<void> {
191191
const deadline = Date.now() + timeoutMs;
192+
const FETCH_TIMEOUT_MS = 2_000;
192193
let lastFetchErr: unknown = null;
193-
let lastCurlErr: unknown = null;
194-
let attemptsSinceCurl = 0;
195-
let curlSucceededOnce = false;
194+
let fetchAttempts = 0;
195+
196196
while (Date.now() < deadline) {
197197
try {
198-
const res = await fetch(`${url}/doc`, { method: "GET" });
198+
fetchAttempts++;
199+
const res = await fetch(`${url}/doc`, {
200+
method: "GET",
201+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
202+
});
199203
if (res.ok || res.status === 404 || res.status === 401) {
200204
// Server is responding — any HTTP response means it booted.
201205
return;
202206
}
203207
} catch (err) {
204208
lastFetchErr = err;
205209
}
206-
attemptsSinceCurl++;
207-
// Every ~150 fetch attempts (≈30s at 200ms cadence) try curl as
208-
// a Bun-fetch-independent probe.
209-
if (attemptsSinceCurl >= 150) {
210-
attemptsSinceCurl = 0;
211-
try {
212-
const probe = Bun.spawnSync({
213-
cmd: ["curl", "-fsS", "--max-time", "2", `${url}/doc`],
214-
stdout: "pipe",
215-
stderr: "pipe",
216-
});
217-
if (probe.exitCode === 0) {
218-
curlSucceededOnce = true;
219-
console.warn(
220-
`[waitForReady] curl reached ${url}/doc but Bun fetch is still failing — proceeding (Bun fetch issue, not opencode).`,
221-
);
222-
return;
223-
}
224-
lastCurlErr = new Error(
225-
`curl exit=${probe.exitCode}: ${probe.stderr.toString().trim() || "(no stderr)"}`,
226-
);
227-
} catch (err) {
228-
lastCurlErr = err;
229-
}
230-
}
231210
await Bun.sleep(200);
232211
}
233212
throw new Error(
234213
`opencode serve did not become ready in ${timeoutMs}ms.\n` +
235214
` url=${url}/doc\n` +
236-
` fetchLastErr=${String(lastFetchErr)}\n` +
237-
` curlLastErr=${String(lastCurlErr)}\n` +
238-
` curlEverSucceeded=${curlSucceededOnce}`,
215+
` fetchAttempts=${fetchAttempts}\n` +
216+
` fetchLastErr=${String(lastFetchErr)}`,
239217
);
240218
}
241219

0 commit comments

Comments
 (0)