{"record":{"id":"325f41ca732906db","repo":"can1357/oh-my-pi","slug":"aborted-325f41","errorCode":null,"errorMessage":"Aborted","messagePattern":"Aborted","errorType":"exception","errorClass":"ToolAbortError","httpStatus":null,"severity":"info","filePath":"packages/coding-agent/src/web/scrapers/types.ts","lineNumber":145,"sourceCode":"\t\t\treturn new TextDecoder(label as Bun.Encoding).decode(bytes);\n\t\t} catch {\n\t\t\t// Unknown/unsupported label — fall back to UTF-8.\n\t\t}\n\t}\n\treturn bytes.toString(\"utf-8\");\n}\n\n/**\n * Fetch a page with timeout and size limit\n */\nexport async function loadPage(url: string, options: LoadPageOptions = {}): Promise<LoadPageResult> {\n\tconst { timeout = 20, headers = {}, maxBytes = MAX_BYTES, signal, method = \"GET\", body } = options;\n\n\tlet lastError: string | undefined;\n\tlet retried429 = false;\n\tfor (let attempt = 0; attempt < USER_AGENTS.length; attempt++) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new ToolAbortError();\n\t\t}\n\n\t\tconst userAgent = USER_AGENTS[attempt];\n\t\tconst requestSignal = ptree.combineSignals(signal, timeout * 1000);\n\n\t\ttry {\n\t\t\tconst requestInit: RequestInit = {\n\t\t\t\tsignal: requestSignal,\n\t\t\t\tmethod,\n\t\t\t\theaders: {\n\t\t\t\t\t\"User-Agent\": userAgent,\n\t\t\t\t\tAccept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n\t\t\t\t\t\"Accept-Language\": \"en-US,en;q=0.5\",\n\t\t\t\t\t\"Accept-Encoding\": \"identity\", // Cloudflare Markdown-for-Agents returns corrupted bytes when compression is negotiated\n\t\t\t\t\t...headers,\n\t\t\t\t},\n\t\t\t\tredirect: \"follow\",\n\t\t\t};","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/scrapers/types.ts#L127-L163","documentation":"loadPage, the shared fetch helper for all web-fetch special handlers, checks `signal?.aborted` at the top of each user-agent retry iteration and throws ToolAbortError immediately if the caller's signal is already cancelled. This is deliberate: a fetch would still be attempted with an aborted combined signal, so the helper short-circuits with the canonical tool-cancellation error.","triggerScenarios":"Any handler (result, response, altResult, readmeResult, postResult, topicResult — e.g. GitHub, Reddit, HN scrapers) calling loadPage after its AbortSignal has fired: user pressed Esc, a timeout elapsed, session shutdown, or a 429 backoff wait was interrupted. Also fires when the signal aborts between user-agent attempts during bot-block retry rotation.","commonSituations":"User cancels a slow page fetch mid user-agent rotation; combined timeout signal (timeout*1000 via ptree.combineSignals) expires between attempts; Retry-After backoff for a 429 overlaps signal abort; upstream very slow so users cancel routinely.","solutions":["No fix needed — this is intentional cancellation; catch ToolAbortError in the caller and stop work.","If it fires without user action, audit the AbortSignal source for premature aborts (short timeouts, leaked controllers aborted elsewhere).","Increase the timeout option passed to loadPage/web-fetch so long fetches complete before the combined timeout signal fires.","Avoid reusing an already-aborted AbortController across queued fetches; create a fresh controller per request.","Check callers for aborting a shared controller on one failed request while other requests still need it."],"exampleFix":"// before: one shared controller for a batch; one abort kills every loadPage call\nconst controller = new AbortController();\nfor (const url of urls) await loadPage(url, { signal: controller.signal });\n// after: per-request controller with its own timeout\nfor (const url of urls) {\n  const controller = new AbortController();\n  try {\n    await loadPage(url, { signal: controller.signal, timeout: 20 });\n  } catch (err) {\n    if (err instanceof ToolAbortError) break; // only this request's signal fired\n    throw err;\n  }\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) return; // avoid calling loadPage with a pre-aborted signal","typeGuard":"function isAbort(err: unknown): err is ToolAbortError {\n  return err instanceof ToolAbortError;\n}","tryCatchPattern":"try {\n  const page = await loadPage(url, { signal, timeout: 20 });\n} catch (err) {\n  if (err instanceof ToolAbortError) return; // cancellation — stop, don't retry\n  throw err;\n}","preventionTips":["Check signal.aborted before each queued loadPage call.","Use a fresh AbortController per request; never share one across a batch.","Never abort a controller as an error-handling side effect on unrelated requests.","Set the timeout option deliberately — it feeds the combined timeout signal that triggers aborts.","Catch ToolAbortError explicitly and treat it as cancellation, never as a retryable failure."],"tags":["abort","cancellation","timeout","fetch"],"backgroundTag":"operation-aborted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}