{"record":{"id":"a5c59c3bfaff2dc7","repo":"can1357/oh-my-pi","slug":"request-was-aborted-a5c59c","errorCode":null,"errorMessage":"Request was aborted","messagePattern":"Request was aborted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"packages/utils/src/fetch-retry.ts","lineNumber":201,"sourceCode":" */\nexport async function fetchWithRetry(\n\turl: string | URL | ((attempt: number) => string | URL),\n\toptions: FetchWithRetryOptions = {},\n): Promise<Response> {\n\tconst {\n\t\tmaxAttempts = DEFAULT_MAX_ATTEMPTS,\n\t\tmaxDelayMs = DEFAULT_MAX_DELAY_MS,\n\t\tdefaultDelayMs,\n\t\tprepareInit,\n\t\tshouldRetryResponse,\n\t\tfetch: fetchImpl = fetch,\n\t\ttimeout = false,\n\t\t...baseInit\n\t} = options;\n\tconst signal = baseInit.signal as AbortSignal | undefined;\n\n\tfor (let attempt = 0; ; attempt++) {\n\t\tif (signal?.aborted) throw new Error(\"Request was aborted\");\n\t\tconst requestUrl = typeof url === \"function\" ? url(attempt) : url;\n\t\t// `timeout` is destructured out of `baseInit`, so forward it to the underlying\n\t\t// fetch on the no-`prepareInit` path too. Without this, callers that pass\n\t\t// `timeout: false` (every streaming provider, to disable Bun's native ~300s\n\t\t// fetch ceiling in favor of their own first-event/idle watchdog) had it\n\t\t// silently dropped, so long-running streams were killed at ~300s (issue #602).\n\t\t// Only forward when the caller actually set `timeout`, so callers that never\n\t\t// set it keep Bun's default ceiling.\n\t\tconst init = prepareInit\n\t\t\t? mergeInit(baseInit, await prepareInit(attempt), timeout)\n\t\t\t: \"timeout\" in options\n\t\t\t\t? ({ ...baseInit, timeout } as unknown as RequestInit)\n\t\t\t\t: baseInit;\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetchImpl(requestUrl, init);\n\t\t} catch (error) {","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/fetch-retry.ts#L183-L219","documentation":"fetchWithRetry checks the caller-supplied AbortSignal at the top of every retry loop attempt. If the signal is already aborted (or was aborted between attempts), it throws Error('Request was aborted') instead of issuing or retrying the request. The library deliberately converts aborts into this plain Error so callers can distinguish user/caller cancellation from transient network failures, which are wrapped and retried instead.","triggerScenarios":"Calling fetchWithRetry with options.signal that is already aborted before the first attempt, or aborting the signal (via AbortController.abort(), AbortSignal.timeout(), or a composed signal) while the loop is between attempts or after a failed fetch. Also fires when a shared/parent signal aborts mid-retry.","commonSituations":"User cancels a long LLM request; a timeout watchdog (AbortSignal.timeout) expires during retry backoff; a request is composed with AbortSignal.any and another branch aborted; retrying a request after the surrounding operation was cancelled.","solutions":["Check signal.aborted before calling and skip/handle the request if already aborted","Pass an AbortController you control and only abort intentionally; treat this Error as normal cancellation, not a bug","If aborts are unintended, inspect all signals combined via AbortSignal.any for premature timeout values","Wrap the call in try/catch and branch on message === 'Request was aborted' to suppress cancellation noise"],"exampleFix":"// before\nconst res = await fetchWithRetry(url, { signal: controller.signal });\n// after\nif (controller.signal.aborted) return; // don't start an already-cancelled request\ntry {\n  const res = await fetchWithRetry(url, { signal: controller.signal });\n} catch (err) {\n  if (err instanceof Error && err.message === \"Request was aborted\") return; // cancellation\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) return; // skip before starting","typeGuard":"function isAbortError(err: unknown): boolean {\n  return err instanceof Error && err.message === \"Request was aborted\";\n}","tryCatchPattern":"try {\n  const res = await fetchWithRetry(url, { signal });\n} catch (err) {\n  if (isAbortError(err)) return; // expected cancellation\n  throw err;\n}","preventionTips":["Check signal.aborted before initiating","Create a dedicated AbortController per request instead of sharing signals","Keep timeout budgets larger than total expected retry+backoff duration"],"tags":["abort","fetch","cancellation"],"backgroundTag":"request-aborted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}