cjpais/Handy · error · anyhow::Error

download incomplete: expected

Error message

download incomplete: expected {} bytes, got {}

What it means

Raised after the body stream ends normally but the on-disk file size does not match the expected total — the server closed the connection early without signaling an error. The size check is the last line of defense before hash verification; a short file can never pass the SHA256 gate anyway. The incomplete partial is deleted, so a retry restarts from zero (a too-short stream taints the whole append).

Solutions

  1. Retry the download — the incomplete file is already deleted.
  2. Keep the models directory on local disk, not cloud-synced storage.
  3. If it fails at the same percentage repeatedly, test the URL with curl to see where the server cuts off; then switch sources.
  4. Rule out disk-full: the check needs room for the full expected size.
Defensive patterns

Strategy: retry

Try / catch

match manager.download_model(model_id, None).await {
    Ok(_) => Ok(()),
    Err(e) if e.to_string().contains("download incomplete") => {
        // short stream: partial deleted; retry from scratch
        manager.download_model(model_id, None).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Server closes the connection mid-body (Content-Length larger than what is sent); proxy cuts the transfer; keep-alive reuse races; cloud-sync folder pinning/unpinning the file as it is written.

Common situations: Flaky connections that drop without RST reaching the client; CDN nodes failing mid-stream; AV quarantining the partial before completion; writing model files into a synced folder.

Related errors


AI-assisted analysis of cjpais/Handy@c89b7bf389 (2026-08-17). Data as JSON: /api/errors/f6557ba08d2c6cc8. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/managers/model/download.rs:368

                    ));
                }
            }
            file.write_all(&chunk)?;
            downloaded += chunk.len() as u64;
            if last_emit.elapsed() >= throttle {
                emit_progress(downloaded);
                last_emit = Instant::now();
            }
        }
        file.flush()?;
        drop(file);
        emit_progress(downloaded);

        if let Some(expected) = known_total {
            let actual = partial_path.metadata()?.len();
            if actual != expected {
                let _ = fs::remove_file(partial_path);
                return Err(anyhow::anyhow!(
                    "download incomplete: expected {} bytes, got {}",
                    expected,
                    actual
                ));
            }
        }

        // The catalog hash is the trust anchor: for a mirror (an untrusted
        // host) this verification is what makes the fallback safe at all.
        Self::verify_file_with_events(model_id, partial_path, expected_sha256, emit).await?;
        Ok(HttpDownloadOutcome::Completed)
    }
}

#[cfg(test)]
mod tests;

View on GitHub (pinned to c89b7bf389)