{"record":{"id":"a916ba4b7371924b","repo":"can1357/oh-my-pi","slug":"aborted-a916ba","errorCode":null,"errorMessage":"Aborted","messagePattern":"Aborted","errorType":"exception","errorClass":"ToolAbortError","httpStatus":null,"severity":"warning","filePath":"packages/coding-agent/src/web/scrapers/utils.ts","lineNumber":42,"sourceCode":"\tok: true;\n\tbuffer: Uint8Array;\n\tcontentDisposition?: string;\n}\n\nexport type BinaryFetchResult = BinaryFetchSuccess | { ok: false; error?: string };\n\nasync function readResponseWithLimit(response: Response, maxBytes: number, signal?: AbortSignal): Promise<Uint8Array> {\n\tconst reader = response.body?.getReader();\n\tif (!reader) return new Uint8Array(0);\n\n\tconst chunks: Buffer[] = [];\n\tlet totalBytes = 0;\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tawait reader.cancel();\n\t\t\t\tthrow new ToolAbortError();\n\t\t\t}\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tif (!value || value.byteLength === 0) continue;\n\n\t\t\ttotalBytes += value.byteLength;\n\t\t\tif (totalBytes > maxBytes) {\n\t\t\t\tawait reader.cancel();\n\t\t\t\tthrow new Error(`response exceeds ${maxBytes} bytes`);\n\t\t\t}\n\n\t\t\tchunks.push(Buffer.from(value));\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n\n\treturn new Uint8Array(Buffer.concat(chunks, totalBytes));","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/scrapers/utils.ts#L24-L60","documentation":"readResponseWithLimit streams the response body chunk-by-chunk and checks the AbortSignal before every read. When the signal is aborted it cancels the reader and throws ToolAbortError('Aborted') so body reads don't hang past cancellation. This propagates the caller's cancellation into the body-streaming phase.","triggerScenarios":"AbortSignal fires while the response body is still being streamed into the size-capped reader loop — the next loop iteration detects signal.aborted.","commonSituations":"Large/slow downloads (PDFs, images) cancelled by a tool timeout or user abort halfway through the body.","solutions":["Raise the caller's timeout if large payloads legitimately take long.","Catch ToolAbortError around readResponseWithLimit/fetchBinary and treat as cancellation.","Verify upstream code isn't aborting early (e.g. a race with an already-resolved promise).","Pass undefined signal only if you truly want an unabortable read — otherwise keep the signal and handle the error."],"exampleFix":"// before\nconst buf = await readResponseWithLimit(res, MAX, signal);\n// after\nlet buf;\ntry { buf = await readResponseWithLimit(res, MAX, signal); }\ncatch (e) { if (e instanceof ToolAbortError) return { ok: false, error: \"aborted\" }; throw e; }","handlingStrategy":"try-catch","validationCode":"// Nothing to pre-validate; optionally abort early:\nif (signal?.aborted) return { ok: false, error: \"aborted\" };","typeGuard":"import { ToolAbortError } from \"../tools/tool-errors\"; const isToolAbortError = (e: unknown): e is ToolAbortError => e instanceof ToolAbortError;","tryCatchPattern":"try { const buf = await readResponseWithLimit(res, MAX, signal); }\ncatch (e) {\n  if (isToolAbortError(e)) return { ok: false, error: \"aborted\" };\n  if (e instanceof Error && /response exceeds/.test(e.message)) return { ok: false, error: \"too-large\" };\n  throw e;\n}","preventionTips":["Keep the AbortSignal threaded through body reads, not just the fetch call.","Handle abort distinctly from size-limit errors — they share the code path but mean different things.","For big payloads, prefer Content-Length pre-checks over discovering overflow mid-stream."],"tags":["abort","streaming","cancellation"],"backgroundTag":"operation-aborted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}