{"record":{"id":"540f74dc560ec733","repo":"cjpais/Handy","slug":"transfer-stalled-no-data-for-s","errorCode":null,"errorMessage":"transfer stalled: no data for {}s","messagePattern":"transfer stalled: no data for (.+?)s","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/model/download.rs","lineNumber":327,"sourceCode":"                total: total_size,\n                percentage: if total_size > 0 {\n                    (downloaded as f64 / total_size as f64) * 100.0\n                } else {\n                    0.0\n                },\n            }));\n        };\n        emit_progress(downloaded);\n\n        // Throttle progress events to max 10/sec (100ms intervals)\n        let mut last_emit = Instant::now();\n        let throttle = Duration::from_millis(100);\n        let mut stream = response.bytes_stream();\n        loop {\n            let chunk = tokio::select! {\n                c = tokio::time::timeout(DOWNLOAD_STALL_TIMEOUT, stream.next()) => match c {\n                    // Stalled mid-body: keep the partial for resume.\n                    Err(_) => return Err(anyhow::anyhow!(\n                        \"transfer stalled: no data for {}s\",\n                        DOWNLOAD_STALL_TIMEOUT.as_secs()\n                    )),\n                    Ok(None) => break,\n                    Ok(Some(chunk)) => chunk?,\n                },\n                _ = cancel_token.cancelled() => {\n                    // Keep the partial for resume; caller handles state cleanup.\n                    return Ok(HttpDownloadOutcome::Cancelled);\n                }\n            };\n            // An untrusted server must not be able to fill the disk: cut the\n            // transfer at the first byte past the known total instead of\n            // trusting it to eventually close the stream. Everything written\n            // so far is tainted by a provably-misbehaving server — clear it.\n            if let Some(cap) = known_total {\n                if downloaded + chunk.len() as u64 > cap {\n                    drop(file);","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/cjpais/Handy/blob/98a4d80cce8ad41efec2a419b59d9e81229a35d7/src-tauri/src/managers/model/download.rs#L309-L345","documentation":"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.","triggerScenarios":"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.","commonSituations":"Multi-GB model downloads over unreliable links; VPNs with aggressive idle timers; laptops sleeping mid-download.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nloop {\n    attempts += 1;\n    match downloader.download_http_resumable(...).await {\n        Ok(outcome) => break Ok(outcome),\n        Err(e) if e.to_string().starts_with(\"transfer stalled\") && attempts < 5 => {\n            // partial is preserved — resume continues from the last byte\n            tokio::time::sleep(Duration::from_secs(3)).await;\n            continue;\n        }\n        Err(e) => break Err(e),\n    }\n}","preventionTips":["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"],"tags":["network","idle-timeout","stall","resume","download"],"backgroundTag":"network-idle-timeout","analyzedSha":"98a4d80cce8ad41efec2a419b59d9e81229a35d7","analyzedAt":"2026-08-16T20:58:09.966Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}