cjpais/Handy · error · anyhow::Error
Download verification failed for model
Error message
Download verification failed for model {}: file is corrupt. Please retry. What it means
Raised after an HTTP model download completes but the file's SHA256 does not match the catalog's expected hash. This is the trust anchor for downloads (especially from untrusted mirrors): the bytes on disk are not the bytes the catalog signed off on. The corrupted file is deleted automatically before the error is returned, so a plain retry starts clean.
Solutions
- Simply retry the download — the corrupt file is already deleted and the next attempt starts fresh.
- If it fails twice at the same offset/hash, switch network (avoid intercepting proxies/VPNs) and retry.
- Try the primary source instead of the mirror (or vice versa) — a consistent mismatch on one host means that host's object is wrong.
- If a specific model always mismatches, the catalog hash may be outdated — report it so the hash gets regenerated.
- Rule out antivirus/disk-level tampering by temporarily excluding the models directory.
Example fix
// before
manager.download_http_resumable(id, url, &partial, size, sha, &emit).await?;
// after — hash failures are self-cleaning; retry a bounded number of times
let mut last = None;
for _ in 0..3 {
match manager.download_http_resumable(id, url, &partial, size, sha, &emit).await {
Ok(out) => break,
Err(e) if e.to_string().contains("corrupt") => { last = Some(e); continue; }
Err(e) => return Err(e),
}
}
if let Some(e) = last { return Err(e); } Defensive patterns
Strategy: retry
Try / catch
let mut attempt = 0;
loop {
attempt += 1;
match manager.download_model(model_id, None).await {
Ok(_) => break,
Err(e) if e.to_string().contains("corrupt") && attempt < 3 => {
warn!("attempt {attempt}: {e}; retrying (file was auto-deleted)");
continue;
}
Err(e) => return Err(e),
}
} Prevention
- Rely on the built-in SHA256 gate — never bypass verification for faster downloads.
- Prefer the catalog's primary URL; treat mirrors as fallback only (their safety depends on this hash check).
- Exclude the models directory from antivirus real-time scanning to prevent byte rewriting.
- Retry with a fresh network path when two consecutive attempts mismatch.
When it happens
Trigger: verify_sha256 computes a hash that != expected: truncated body that still matched Content-Length (lying server), corrupted transfer via a proxy/AV scanner, a mirror serving a tampered or different build, or bit-rot on disk during hashing.
Common situations: Corporate proxies or antivirus re-writing downloads; Hugging Face/mirror serving a changed file while the catalog hash is stale; flaky Wi-Fi corrupting chunks that still pass size checks; disk issues on large GGUF files.
Related errors
- Failed to verify download for model
- Complete model file not found
- download incomplete: expected
- Model is currently downloading
- server object ends before the expected size (HTTP 416)
AI-assisted analysis of cjpais/Handy@c89b7bf389 (2026-08-17).
Data as JSON: /api/errors/5bcfa3445f75b4e9.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/managers/model/download.rs:71
/// On mismatch or read error the partial file is deleted and an error is returned,
/// so the next download attempt always starts from a clean state.
/// When `expected_sha256` is `None` (custom user models) verification is skipped.
fn verify_sha256(path: &Path, expected_sha256: Option<&str>, model_id: &str) -> Result<()> {
let Some(expected) = expected_sha256 else {
return Ok(());
};
match Self::compute_sha256(path) {
Ok(actual) if actual == expected => {
info!("SHA256 verified for model {}", model_id);
Ok(())
}
Ok(actual) => {
warn!(
"SHA256 mismatch for model {}: expected {}, got {}",
model_id, expected, actual
);
let _ = fs::remove_file(path);
Err(anyhow::anyhow!(
"Download verification failed for model {}: file is corrupt. Please retry.",
model_id
))
}
Err(e) => {
let _ = fs::remove_file(path);
Err(anyhow::anyhow!(
"Failed to verify download for model {}: {}. Please retry.",
model_id,
e
))
}
}
}
/// Computes the SHA256 hex digest of a file, reading in 64KB chunks to handle large models.
fn compute_sha256(path: &Path) -> Result<String> {
let mut file = File::open(path)?;View on GitHub (pinned to c89b7bf389)