jdx/mise · info
download cancelled by user
Error message
download cancelled by user
What it means
Raised when the user presses Ctrl-C (or otherwise signals cancellation) while an HTTP download body is being streamed to a partial file. The download loop checks a global cancellation flag before writing each chunk and aborts with this error. The partial file is truncated if nothing had been written yet, so resumable downloads can continue from the written offset on retry.
Solutions
- Re-run the install command; partial downloads resume from the written offset
- Check network speed/stability if cancellations are accidental (timeouts prompting manual aborts)
- In CI, increase job timeout or pre-install tools in a cached layer to avoid long-running downloads
- Use a mirror or faster network (`MISE_...` mirror settings) if downloads regularly need interrupting
Defensive patterns
Strategy: retry
Try / catch
// resumable download: just retry the install
if err.contains("download cancelled by user") {
// partial file kept at write_offset; re-running resumes
retry_install();
} Prevention
- Pre-install tools in cached CI layers so downloads are not interrupted by job timeouts
- Avoid Ctrl-C during installs; let mise handle resume on the next run
- Use mirrors for large downloads to shorten transfer windows
When it happens
Trigger: User hits Ctrl-C during `mise install`/backend HTTP download while `resp.chunk()` streaming is in progress and `crate::ui::ctrlc::is_cancelled()` returns true.
Common situations: Developer interrupts a slow tool download (large Node/Python archives, slow network) mid-transfer; CI job timeout sends SIGINT to mise.
Related errors
- HTTP download timed out after
- should not be called for system tool
- blob chunk upload failed
- blob upload failed
- {}
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/1b8fe53e909d0e92.
Report an issue: GitHub.
Appendix: source
Thrown at src/http.rs:1232
}
if let Some(pr) = pr {
if let Some(total_size) = total_size {
pr.set_length(total_size);
}
pr.set_position(write_offset);
}
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.append(write_offset > 0)
.truncate(write_offset == 0)
.open(&partial.path)
.await?;
let transfer = async {
while let Some(chunk) = resp.chunk().await? {
if crate::ui::ctrlc::is_cancelled() {
bail!("download cancelled by user");
}
file.write_all(&chunk).await?;
bytes_received.fetch_add(chunk.len() as u64, Ordering::Relaxed);
if let Some(pr) = pr {
pr.inc(chunk.len() as u64);
}
}
Ok::<_, Report>(())
}
.await;
// Outside the transfer, so it also runs when the transfer failed.
// `tokio::fs::File` buffers writes and dropping one does not flush,
// so a transfer that dies part way through was leaving the bytes it
// had already received unwritten — the resume then asked for a range
// starting before them and downloaded them again. Whether any of it
// survived depended on when the background flush happened to land.
let persisted = async {
file.shutdown().await?;View on GitHub (pinned to 533346cc37)