{"record":{"id":"52a302b503dbfbfb","repo":"cjpais/Handy","slug":"server-advertises-bytes-expected","errorCode":null,"errorMessage":"server advertises {} bytes, expected {}","messagePattern":"server advertises (.+?) bytes, expected (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/model/download.rs","lineNumber":285,"sourceCode":"            let starts_at = response\n                .headers()\n                .get(reqwest::header::CONTENT_RANGE)\n                .and_then(|v| v.to_str().ok())\n                .and_then(content_range_start);\n            if starts_at != Some(resume_from) {\n                let _ = fs::remove_file(partial_path);\n                return Err(anyhow::anyhow!(\n                    \"server returned Content-Range starting at {:?}, expected {}\",\n                    starts_at,\n                    resume_from\n                ));\n            }\n        }\n        // When the catalog pins the size, a server advertising a different\n        // total is already misbehaving — reject before writing anything.\n        if let (Some(expected), Some(len)) = (expected_size, response.content_length()) {\n            if resume_from + len != expected {\n                return Err(anyhow::anyhow!(\n                    \"server advertises {} bytes, expected {}\",\n                    resume_from + len,\n                    expected\n                ));\n            }\n        }\n\n        let known_total =\n            expected_size.or_else(|| response.content_length().map(|l| resume_from + l));\n        let total_size = known_total.unwrap_or(0);\n        let mut downloaded = resume_from;\n        let mut file = if resume_from > 0 {\n            std::fs::OpenOptions::new()\n                .append(true)\n                .open(partial_path)?\n        } else {\n            std::fs::File::create(partial_path)?\n        };","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/cjpais/Handy/blob/98a4d80cce8ad41efec2a419b59d9e81229a35d7/src-tauri/src/managers/model/download.rs#L267-L303","documentation":"When the catalog pins an expected_size and the server advertises a Content-Length, the downloader rejects the response up front if resume_from + content_length != expected. The server's object provably differs in size from the pinned model, so writing anything would produce a wrong file. Unlike the taint paths, this branch does not delete the partial.","triggerScenarios":"Upstream re-published the model at a different size while the catalog still pins the old size; a mirror serving a different build; a proxy truncating or padding Content-Length.","commonSituations":"Catalog/app version skew after a model update; third-party mirrors that rebuild files; resuming against a host whose object changed since the partial was created.","solutions":["Delete the leftover partial and retry a fresh download against the current object","Verify: curl -sI <url> and compare Content-Length with the catalog size","Update the app so catalog metadata matches the published model, or pin the matching HF revision","Prefer the official HuggingFace source, where sizes are pinned per revision"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before resuming, confirm the server object still matches the pinned size\nasync fn size_matches(url: &str, resume_from: u64, expected: u64) -> bool {\n    let resp = reqwest::Client::new()\n        .get(url)\n        .header(\"Range\", format!(\"bytes={}-\", resume_from))\n        .send()\n        .await\n        .ok()?;\n    match resp.content_length() {\n        Some(len) => resume_from + len == expected,\n        None => false,\n    }\n}","typeGuard":null,"tryCatchPattern":"match downloader.download_http_resumable(...).await {\n    Ok(outcome) => Ok(outcome),\n    Err(e) if e.to_string().starts_with(\"server advertises\") => {\n        // metadata/size skew: drop the partial and refresh catalog before retrying\n        remove_partial(partial_path);\n        refresh_catalog_metadata().await?;\n        downloader.download_http_resumable(...).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Pin sizes (and hashes) per immutable revision, not per mutable 'latest' URL","Compare HEAD Content-Length against the catalog before resuming long downloads","Update the catalog whenever an upstream model file is re-published"],"tags":["content-length","size-mismatch","download","resume"],"backgroundTag":"content-length-mismatch","analyzedSha":"98a4d80cce8ad41efec2a419b59d9e81229a35d7","analyzedAt":"2026-08-16T20:58:09.966Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}