fix(ui): bound OpenCode read requests so half-open sockets cannot freeze bootstrap (#2470)

The SDK client fetch wrapper now applies a 30s timeout to non-streaming
reads. Without it, a socket that neither resolves nor rejects keeps the
directory bootstrap concurrency slot busy forever and the UI stays on
"loading sessions". Long-lived streams (POST prompts, the /event SSE)
are explicitly excluded so they are not cut off mid-flight.

The normalized "request timed out" error is added to the retry
allowlist alongside undici's "terminated" (the exact failure observed
in #2470 when undici tears down a half-open upstream connection); both
are transient while the managed OpenCode process restarts. Caller-
initiated aborts keep their original error shape so a user-cancelled
request is not retried.

Tests cover: GET timeout fires after the bound, POST is not timed out,
/event SSE is not timed out, caller abort wins, AbortError is not
retried, and the SDK normalized error is retried 3x.
This commit is contained in:
herjarsa
2026-08-17 11:37:20 +02:00
parent 7ef6441bf3
commit 39bb71a62b
5 changed files with 264 additions and 3 deletions
+6
View File
@@ -6,6 +6,10 @@ export interface RetryOptions {
retryIf?: (error: unknown) => boolean
}
// undici tears down half-open upstream connection with `TypeError: terminated`
// (exact failure from the #2470 logs); the SDK client also rejects reads with
// normalized "request timed out" error after OPENCODE_REQUEST_TIMEOUT_MS.
// Both are transient — managed process may be restarting.
const TRANSIENT_MESSAGES = [
"load failed",
"network connection was lost",
@@ -18,6 +22,8 @@ const TRANSIENT_MESSAGES = [
"opencode api unavailable",
"503",
"502",
"terminated",
"request timed out",
]
function isTransientError(error: unknown): boolean {