cjpais/Handy · error · anyhow::Error
transfer stalled: no data for {}s
Error message
transfer stalled: no data for {}s What it means
Mid-body watchdog: no chunk arrived from the byte stream for DOWNLOAD_STALL_TIMEOUT (60s). This is tokio::time::timeout applied per-chunk inside the download loop. Unlike a hard failure, the partial file is deliberately kept so the next attempt resumes from the current offset; a cancellation token fires the Cancelled outcome instead of this error.
Source
Thrown at src-tauri/src/managers/model/download.rs:327
total: total_size,
percentage: if total_size > 0 {
(downloaded as f64 / total_size as f64) * 100.0
} else {
0.0
},
}));
};
emit_progress(downloaded);
// Throttle progress events to max 10/sec (100ms intervals)
let mut last_emit = Instant::now();
let throttle = Duration::from_millis(100);
let mut stream = response.bytes_stream();
loop {
let chunk = tokio::select! {
c = tokio::time::timeout(DOWNLOAD_STALL_TIMEOUT, stream.next()) => match c {
// Stalled mid-body: keep the partial for resume.
Err(_) => return Err(anyhow::anyhow!(
"transfer stalled: no data for {}s",
DOWNLOAD_STALL_TIMEOUT.as_secs()
)),
Ok(None) => break,
Ok(Some(chunk)) => chunk?,
},
_ = cancel_token.cancelled() => {
// Keep the partial for resume; caller handles state cleanup.
return Ok(HttpDownloadOutcome::Cancelled);
}
};
// An untrusted server must not be able to fill the disk: cut the
// transfer at the first byte past the known total instead of
// trusting it to eventually close the stream. Everything written
// so far is tainted by a provably-misbehaving server — clear it.
if let Some(cap) = known_total {
if downloaded + chunk.len() as u64 > cap {
drop(file);View on GitHub (pinned to 98a4d80cce)
Solutions
- Retry — resume continues from the preserved partial instead of restarting
- Stabilize the network: wired link, disable aggressive VPN idle timers
- On genuinely slow links, raise DOWNLOAD_STALL_TIMEOUT (download.rs:26)
- Prevent system sleep during large downloads
Defensive patterns
Strategy: retry
Try / catch
let mut attempts = 0;
loop {
attempts += 1;
match downloader.download_http_resumable(...).await {
Ok(outcome) => break Ok(outcome),
Err(e) if e.to_string().starts_with("transfer stalled") && attempts < 5 => {
// partial is preserved — resume continues from the last byte
tokio::time::sleep(Duration::from_secs(3)).await;
continue;
}
Err(e) => break Err(e),
}
} Prevention
- Wrap downloads in a bounded auto-resume loop; stalls preserve the partial by design
- Keep VPN/NAT idle timeouts above the 60s stall budget or send keepalive traffic
- Prevent system sleep during multi-GB model downloads
When it happens
Trigger: Connection silently dropped (NAT/VPN idle timeout, Wi-Fi flap) so reads hang instead of erroring; server stalling mid-body; system sleep pausing the transfer.
Common situations: Multi-GB model downloads over unreliable links; VPNs with aggressive idle timers; laptops sleeping mid-download.
Related errors
- transfer stalled: no progress for {}s
- Hugging Face download failed after {} attempt(s): {}
- Download failed from Hugging Face ({}) and {} mirror(s)
- no response within {}s from {}
- server object ends before the expected size (HTTP 416)
AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16).
Data as JSON: /api/errors/540f74dc560ec733.
Report an issue: GitHub.