{"record":{"id":"f6557ba08d2c6cc8","repo":"cjpais/Handy","slug":"download-incomplete-expected-bytes-got","errorCode":null,"errorMessage":"download incomplete: expected {} bytes, got {}","messagePattern":"download incomplete: expected (.+?) bytes, got (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/model/download.rs","lineNumber":368,"sourceCode":"                    ));\n                }\n            }\n            file.write_all(&chunk)?;\n            downloaded += chunk.len() as u64;\n            if last_emit.elapsed() >= throttle {\n                emit_progress(downloaded);\n                last_emit = Instant::now();\n            }\n        }\n        file.flush()?;\n        drop(file);\n        emit_progress(downloaded);\n\n        if let Some(expected) = known_total {\n            let actual = partial_path.metadata()?.len();\n            if actual != expected {\n                let _ = fs::remove_file(partial_path);\n                return Err(anyhow::anyhow!(\n                    \"download incomplete: expected {} bytes, got {}\",\n                    expected,\n                    actual\n                ));\n            }\n        }\n\n        // The catalog hash is the trust anchor: for a mirror (an untrusted\n        // host) this verification is what makes the fallback safe at all.\n        Self::verify_file_with_events(model_id, partial_path, expected_sha256, emit).await?;\n        Ok(HttpDownloadOutcome::Completed)\n    }\n}\n\n#[cfg(test)]\nmod tests;\n","sourceCodeStart":350,"sourceCodeEnd":385,"githubUrl":"https://github.com/cjpais/Handy/blob/c89b7bf38974ad317177389f5bfe28236f0d64e3/src-tauri/src/managers/model/download.rs#L350-L385","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the download — the incomplete file is already deleted.","Keep the models directory on local disk, not cloud-synced storage.","If it fails at the same percentage repeatedly, test the URL with curl to see where the server cuts off; then switch sources.","Rule out disk-full: the check needs room for the full expected size."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match manager.download_model(model_id, None).await {\n    Ok(_) => Ok(()),\n    Err(e) if e.to_string().contains(\"download incomplete\") => {\n        // short stream: partial deleted; retry from scratch\n        manager.download_model(model_id, None).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Always compare final file size against the expected total before trusting a download (as this code does).","Store models on local disks, not synced/network volumes that truncate or evict files.","Keep enough free disk space for the full expected size plus overhead.","Retry short transfers automatically a bounded number of times before surfacing to the user."],"tags":["download","truncated","size-mismatch","network","model"],"backgroundTag":"truncated-download","analyzedSha":"c89b7bf38974ad317177389f5bfe28236f0d64e3","analyzedAt":"2026-08-17T10:29:55.597Z","contentChangedAt":"2026-08-17T10:29:55.597Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}