cjpais/Handy · error · anyhow::Error
server advertises {} bytes, expected {}
Error message
server advertises {} bytes, expected {} What it means
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.
Source
Thrown at src-tauri/src/managers/model/download.rs:285
let starts_at = response
.headers()
.get(reqwest::header::CONTENT_RANGE)
.and_then(|v| v.to_str().ok())
.and_then(content_range_start);
if starts_at != Some(resume_from) {
let _ = fs::remove_file(partial_path);
return Err(anyhow::anyhow!(
"server returned Content-Range starting at {:?}, expected {}",
starts_at,
resume_from
));
}
}
// When the catalog pins the size, a server advertising a different
// total is already misbehaving — reject before writing anything.
if let (Some(expected), Some(len)) = (expected_size, response.content_length()) {
if resume_from + len != expected {
return Err(anyhow::anyhow!(
"server advertises {} bytes, expected {}",
resume_from + len,
expected
));
}
}
let known_total =
expected_size.or_else(|| response.content_length().map(|l| resume_from + l));
let total_size = known_total.unwrap_or(0);
let mut downloaded = resume_from;
let mut file = if resume_from > 0 {
std::fs::OpenOptions::new()
.append(true)
.open(partial_path)?
} else {
std::fs::File::create(partial_path)?
};View on GitHub (pinned to 98a4d80cce)
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
Defensive patterns
Strategy: validation
Validate before calling
// Before resuming, confirm the server object still matches the pinned size
async fn size_matches(url: &str, resume_from: u64, expected: u64) -> bool {
let resp = reqwest::Client::new()
.get(url)
.header("Range", format!("bytes={}-", resume_from))
.send()
.await
.ok()?;
match resp.content_length() {
Some(len) => resume_from + len == expected,
None => false,
}
} Try / catch
match downloader.download_http_resumable(...).await {
Ok(outcome) => Ok(outcome),
Err(e) if e.to_string().starts_with("server advertises") => {
// metadata/size skew: drop the partial and refresh catalog before retrying
remove_partial(partial_path);
refresh_catalog_metadata().await?;
downloader.download_http_resumable(...).await
}
Err(e) => Err(e),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- server object ends before the expected size (HTTP 416)
- server returned Content-Range starting at {:?}, expected {}
- transfer stalled: no data for {}s
- transfer stalled: no progress for {}s
- Hugging Face download failed after {} attempt(s): {}
AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16).
Data as JSON: /api/errors/52a302b503dbfbfb.
Report an issue: GitHub.