can1357/oh-my-pi · info · Error

Request was aborted

Error message

Request was aborted

What it means

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.

Source

Thrown at packages/utils/src/fetch-retry.ts:201

 */
export async function fetchWithRetry(
	url: string | URL | ((attempt: number) => string | URL),
	options: FetchWithRetryOptions = {},
): Promise<Response> {
	const {
		maxAttempts = DEFAULT_MAX_ATTEMPTS,
		maxDelayMs = DEFAULT_MAX_DELAY_MS,
		defaultDelayMs,
		prepareInit,
		shouldRetryResponse,
		fetch: fetchImpl = fetch,
		timeout = false,
		...baseInit
	} = options;
	const signal = baseInit.signal as AbortSignal | undefined;

	for (let attempt = 0; ; attempt++) {
		if (signal?.aborted) throw new Error("Request was aborted");
		const requestUrl = typeof url === "function" ? url(attempt) : url;
		// `timeout` is destructured out of `baseInit`, so forward it to the underlying
		// fetch on the no-`prepareInit` path too. Without this, callers that pass
		// `timeout: false` (every streaming provider, to disable Bun's native ~300s
		// fetch ceiling in favor of their own first-event/idle watchdog) had it
		// silently dropped, so long-running streams were killed at ~300s (issue #602).
		// Only forward when the caller actually set `timeout`, so callers that never
		// set it keep Bun's default ceiling.
		const init = prepareInit
			? mergeInit(baseInit, await prepareInit(attempt), timeout)
			: "timeout" in options
				? ({ ...baseInit, timeout } as unknown as RequestInit)
				: baseInit;

		let response: Response;
		try {
			response = await fetchImpl(requestUrl, init);
		} catch (error) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Check signal.aborted before calling and skip/handle the request if already aborted
  2. Pass an AbortController you control and only abort intentionally; treat this Error as normal cancellation, not a bug
  3. If aborts are unintended, inspect all signals combined via AbortSignal.any for premature timeout values
  4. Wrap the call in try/catch and branch on message === 'Request was aborted' to suppress cancellation noise

Example fix

// before
const res = await fetchWithRetry(url, { signal: controller.signal });
// after
if (controller.signal.aborted) return; // don't start an already-cancelled request
try {
  const res = await fetchWithRetry(url, { signal: controller.signal });
} catch (err) {
  if (err instanceof Error && err.message === "Request was aborted") return; // cancellation
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (signal?.aborted) return; // skip before starting

Type guard

function isAbortError(err: unknown): boolean {
  return err instanceof Error && err.message === "Request was aborted";
}

Try / catch

try {
  const res = await fetchWithRetry(url, { signal });
} catch (err) {
  if (isAbortError(err)) return; // expected cancellation
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a5c59c3bfaff2dc7. Report an issue: GitHub.