{"record":{"id":"7da8243ccf7663ac","repo":"sigoden/aichat","slug":"aborted-7da824","errorCode":null,"errorMessage":"Aborted.","messagePattern":"Aborted\\.","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"info","filePath":"src/utils/spinner.rs","lineNumber":163,"sourceCode":"where\n    F: Future<Output = Result<T>>,\n{\n    if *IS_STDOUT_TERMINAL {\n        let (done_tx, done_rx) = oneshot::channel();\n        let run_task = async {\n            tokio::select! {\n                ret = task => {\n                    let _ = done_tx.send(());\n                    ret\n                }\n                _ = tokio::signal::ctrl_c() => {\n                    abort_signal.set_ctrlc();\n                    let _ = done_tx.send(());\n                    bail!(\"Aborted!\")\n                },\n                _ = wait_abort_signal(&abort_signal) => {\n                    let _ = done_tx.send(());\n                    bail!(\"Aborted.\");\n                },\n            }\n        };\n        let (task_ret, spinner_ret) = tokio::join!(\n            run_task,\n            run_abortable_spinner(spinner_rx, done_rx, abort_signal.clone())\n        );\n        spinner_ret?;\n        task_ret\n    } else {\n        task.await\n    }\n}\n\nasync fn run_abortable_spinner(\n    mut spinner_rx: UnboundedReceiver<SpinnerEvent>,\n    mut done_rx: oneshot::Receiver<()>,\n    abort_signal: AbortSignal,","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/sigoden/aichat/blob/82976d349ad97ac9aae0655ad631dace5e2a6385/src/utils/spinner.rs#L145-L181","documentation":"abortable_run_with_spinner_rx also races against wait_abort_signal; when an abort signal has already been (or becomes) set through the shared AbortSignal — e.g. a previous Ctrl+C or a programmatic abort — it bails with 'Aborted.' (note the period), distinguishing signal-based aborts from a fresh Ctrl+C ('Aborted!').","triggerScenarios":"Starting or running a spinner-wrapped task after the shared abort signal was set by an earlier Ctrl+C or by abort_signal.abort(); wait_abort_signal resolves and the task is abandoned.","commonSituations":"Reusing the same AbortSignal across sequential operations; pressing Ctrl+C once and observing the next operation abort immediately; a nested abortable task inheriting an aborted signal.","solutions":["Create or reset a fresh AbortSignal for a new run instead of reusing an aborted one","Avoid calling subsequent spinner-wrapped operations after an abort until state is reset","Check is_ctrlc / abort state before launching follow-up work"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// only start a run when the signal isn't already aborted\nif abort_signal.is_aborted() {\n    // skip or reset the signal before starting the task\n}","typeGuard":null,"tryCatchPattern":"match abortable_run_with_spinner_rx(task, spinner, signal).await {\n    Err(e) if e.to_string() == \"Aborted.\" => {\n        eprintln!(\"operation aborted via abort signal; not a failure of the task itself\");\n    }\n    other => other?,\n}","preventionTips":["Create a fresh AbortSignal per independent operation instead of reusing aborted ones","Check/reset abort state before launching follow-up spinner-wrapped work","Distinguish 'Aborted.' (signal) from 'Aborted!' (fresh Ctrl+C) in scripts"],"tags":["cancellation","abort-signal","spinner"],"backgroundTag":"user-interrupt-abort","analyzedSha":"82976d349ad97ac9aae0655ad631dace5e2a6385","analyzedAt":"2026-09-09T18:33:06.139Z","contentChangedAt":"2026-09-09T18:33:06.139Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}