{"record":{"id":"1b8fe53e909d0e92","repo":"jdx/mise","slug":"download-cancelled-by-user","errorCode":null,"errorMessage":"download cancelled by user","messagePattern":"download cancelled by user","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"src/http.rs","lineNumber":1105,"sourceCode":"            }\n\n            if let Some(pr) = pr {\n                if let Some(total_size) = total_size {\n                    pr.set_length(total_size);\n                }\n                pr.set_position(write_offset);\n            }\n            let mut file = tokio::fs::OpenOptions::new()\n                .create(true)\n                .write(true)\n                .append(write_offset > 0)\n                .truncate(write_offset == 0)\n                .open(&partial.path)\n                .await?;\n            let transfer = async {\n                while let Some(chunk) = resp.chunk().await? {\n                    if crate::ui::ctrlc::is_cancelled() {\n                        bail!(\"download cancelled by user\");\n                    }\n                    file.write_all(&chunk).await?;\n                    bytes_received.fetch_add(chunk.len() as u64, Ordering::Relaxed);\n                    if let Some(pr) = pr {\n                        pr.inc(chunk.len() as u64);\n                    }\n                }\n                Ok::<_, Report>(())\n            }\n            .await;\n            // Outside the transfer, so it also runs when the transfer failed.\n            // `tokio::fs::File` buffers writes and dropping one does not flush,\n            // so a transfer that dies part way through was leaving the bytes it\n            // had already received unwritten — the resume then asked for a range\n            // starting before them and downloaded them again. Whether any of it\n            // survived depended on when the background flush happened to land.\n            let persisted = async {\n                file.shutdown().await?;","sourceCodeStart":1087,"sourceCodeEnd":1123,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/http.rs#L1087-L1123","documentation":"While streaming a download to disk, mise checks a cancellation flag after each received chunk. When the user has signalled cancellation (e.g. Ctrl+C), the transfer stops and this error is raised so the partial download can be kept for later resume. It is an intentional user-initiated abort, not a fault.","triggerScenarios":"Pressing Ctrl+C (or otherwise setting the crate::ui::ctrlc cancelled flag) during any mise tool/artifact download; the chunk loop in the download transfer detects it and bails.","commonSituations":"User interrupts a slow or large download to try again later; a script times out and sends SIGINT to a mise install; terminal disconnect during a long install.","solutions":["Re-run the install when ready — mise resumes from the partial download.","If this appears in a non-interactive script, avoid sending SIGINT to mise or run it in a detached process.","Use --raw/offline-style options or pre-fetch artifacts if downloads are too slow."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match install().await {\n    Err(e) if e.to_string().contains(\"download cancelled by user\") => {\n        // expected on Ctrl+C; partial download is kept for resume\n        eprintln!(\"install interrupted; re-run to resume\");\n    }\n    other => other?,\n}","preventionTips":["Don't send SIGINT to mise in scripts; use timeouts with SIGTERM handling if you must.","Pre-fetch large tools before time-boxed CI steps.","Re-run the same install command — resume picks up the partial download."],"tags":["download","user-interrupt","ctrl-c"],"backgroundTag":"user-cancelled-operation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}