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

  1. Simply retry the download — the corrupt file is already deleted and the next attempt starts fresh.
  2. If it fails twice at the same offset/hash, switch network (avoid intercepting proxies/VPNs) and retry.
  3. Try the primary source instead of the mirror (or vice versa) — a consistent mismatch on one host means that host's object is wrong.
  4. If a specific model always mismatches, the catalog hash may be outdated — report it so the hash gets regenerated.
  5. 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

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


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)