{"record":{"id":"74fde55c1f79960b","repo":"earendil-works/pi","slug":"request-aborted-by-user","errorCode":null,"errorMessage":"Request aborted by user","messagePattern":"Request aborted by user","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"packages/agent/src/proxy.ts","lineNumber":190,"sourceCode":"\t\t\t\t\tif (errorData.error) {\n\t\t\t\t\t\terrorMessage = `Proxy error: ${errorData.error}`;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Couldn't parse error response\n\t\t\t\t}\n\t\t\t\tthrow new Error(errorMessage);\n\t\t\t}\n\n\t\t\treader = response.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t\t}\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\tconst data = line.slice(6).trim();\n\t\t\t\t\t\tif (data) {\n\t\t\t\t\t\t\tconst proxyEvent = JSON.parse(data) as ProxyAssistantMessageEvent;\n\t\t\t\t\t\t\tconst event = processProxyEvent(proxyEvent, partial);\n\t\t\t\t\t\t\tif (event) {\n\t\t\t\t\t\t\t\tstream.push(event);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/proxy.ts#L172-L208","documentation":"Mid-stream cancellation check in streamProxy: after each SSE chunk is read from the proxy, the client re-checks options.signal and throws this if it was aborted (proxy.ts:190). The partial AssistantMessage accumulated so far is preserved and delivered as an error event with reason/stopReason 'aborted', so content received before the abort is not lost.","triggerScenarios":"User hits stop while tokens are streaming; the AbortController passed as options.signal fires from a UI unmount, navigation, or timeout between chunk reads; the abort handler also cancels the reader, ending the fetch body.","commonSituations":"Stop/cancel buttons in chat UIs; React components aborting on unmount (including strict-mode double mounts); per-request timeout controllers expiring during long generations.","solutions":["Handle stopReason 'aborted' as a normal outcome and keep the partial content already delivered","Do not auto-retry aborted streams - that undoes the user's cancellation","Ensure only the owner of the controller aborts it; hunt down accidental aborts from timeouts or unmounts","Pass a signal scoped to exactly one stream, never a reused long-lived controller"],"exampleFix":"// before\nfor await (const ev of streamProxy(model, context, opts)) { render(ev); }\n\n// after - keep partial output on abort\nconst stream = streamProxy(model, context, opts);\nconst final = await stream.result;\nif (final.stopReason === \"aborted\") {\n  renderPartial(final); // content array holds everything streamed pre-abort\n}","handlingStrategy":"type-guard","validationCode":"if (options.signal?.aborted) {\n  // don't even start the stream\n  return cancelledStream();\n}\nconst stream = streamProxy(model, context, options);","typeGuard":"const isAbortedMessage = (m: AssistantMessage): m is AssistantMessage & { stopReason: \"aborted\" } =>\n  m.stopReason === \"aborted\";","tryCatchPattern":"// no catch needed: aborts are encoded as events per the StreamFn contract\nconst stream = streamProxy(model, context, opts);\nconst msg = await stream.result;\nif (isAbortedMessage(msg)) {\n  keepPartialContent(msg); // msg.content holds everything streamed pre-abort\n  return;\n}","preventionTips":["Scope one AbortController to exactly one stream and abort it only in response to user intent","Handle stopReason 'aborted' as a first-class outcome in your UI","Never auto-retry an aborted stream"],"tags":["abort","cancellation","abortsignal","sse","streaming","proxy"],"backgroundTag":"abortsignal-cancellation","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}