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

  1. Retry — resume continues from the preserved partial instead of restarting
  2. Stabilize the network: wired link, disable aggressive VPN idle timers
  3. On genuinely slow links, raise DOWNLOAD_STALL_TIMEOUT (download.rs:26)
  4. 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

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


AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16). Data as JSON: /api/errors/540f74dc560ec733. Report an issue: GitHub.