{"record":{"id":"9f52073d96e872c7","repo":"tonhowtf/omniget","slug":"download-cancelled-http-fetcher","errorCode":null,"errorMessage":"Download cancelled","messagePattern":"Download cancelled","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"info","filePath":"src-tauri/omniget-core/src/core/http_fetcher.rs","lineNumber":327,"sourceCode":"        let resp = req.send().await?;\n        if !resp.status().is_success() {\n            return Err(anyhow!(\"HTTP {} downloading {}\", resp.status(), self.url));\n        }\n        let total = header_content_length(resp.headers()).or_else(|| resp.content_length());\n\n        let mut file = tokio::fs::File::create(part_path).await?;\n        let mut downloaded: u64 = 0;\n        let mut stream = resp.bytes_stream();\n\n        let mut last_emit = std::time::Instant::now();\n        let mut anchor_bytes = 0u64;\n        let mut anchor_time = std::time::Instant::now();\n        let mut speed_ema: f64 = 0.0;\n\n        loop {\n            if let Some(token) = &self.cancel {\n                if token.is_cancelled() {\n                    return Err(anyhow!(\"Download cancelled\"));\n                }\n            }\n            match tokio::time::timeout(self.config.read_timeout, stream.next()).await {\n                Ok(Some(Ok(chunk))) => {\n                    file.write_all(&chunk).await?;\n                    downloaded += chunk.len() as u64;\n                    if last_emit.elapsed() >= Duration::from_millis(250) {\n                        let elapsed = anchor_time.elapsed().as_secs_f64();\n                        if elapsed >= 0.3 {\n                            let instant =\n                                (downloaded.saturating_sub(anchor_bytes)) as f64 / elapsed;\n                            speed_ema = if speed_ema > 0.0 {\n                                speed_ema * 0.6 + instant * 0.4\n                            } else {\n                                instant\n                            };\n                            anchor_bytes = downloaded;\n                            anchor_time = std::time::Instant::now();","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/http_fetcher.rs#L309-L345","documentation":"Thrown in download_streaming when the fetcher's cancel token (self.cancel) is observed to be cancelled, checked at the top of each chunk-read loop iteration. This is the library's cooperative cancellation mechanism: it lets a caller abort an in-flight download and returns a distinct, expected error instead of panicking or leaking the task. The .part file is left on disk (resume data may also be retained depending on use_sidecar_resume).","triggerScenarios":"A CancellationToken was passed to the fetcher and .cancel() was called (e.g. user pressed a Cancel button, app shutdown, or a supervisor timed the task out) while download_streaming was between chunks.","commonSituations":"User-initiated cancel in a GUI (Tauri app) mid-download; application shutdown or window close triggering token cancellation; a new download superseding an old one; watchdog cancelling a stalled download.","solutions":["This error signals intentional cancellation — handle it as a control-flow signal, not a failure: match on the message/cancel cause and skip alerting the user.","If cancellation was unintended, audit who calls .cancel() on the token (timeout wrappers, UI handlers) and fix the trigger.","If downloads should resume after cancellation, keep the .part file and re-invoke download with resume/sidecar support enabled.","If you want the error programmatically distinguishable, wrap the call and map this specific message to your own Cancelled error variant."],"exampleFix":"// before\nfetcher.download(&mut progress_tx).await?; // treats cancel as a hard error\n\n// after\nmatch fetcher.download(&mut progress_tx).await {\n    Err(e) if e.to_string().contains(\"Download cancelled\") => {\n        tracing::info!(\"download cancelled by user\");\n    }\n    other => other?,\n}","handlingStrategy":"try-catch","validationCode":"// ensure the token is fresh before starting\nif cancel_token.is_cancelled() {\n    bail!(\"not starting download: token already cancelled\");\n}","typeGuard":null,"tryCatchPattern":"match fetcher.download(&mut tx).await {\n    Err(e) if e.to_string().contains(\"Download cancelled\") => {\n        // expected control flow: clean up UI state, keep .part for resume\n        set_download_state(DownloadState::Cancelled);\n    }\n    other => other?,\n}","preventionTips":["Treat this error as a signal, not a failure — never report it to users as a crash.","Audit all code paths that call .cancel() on the shared token (timeouts, shutdown hooks).","Keep the .part file and sidecar resume data so cancelled downloads can resume.","Cancel the token only when the download task is guaranteed to observe it (task still alive)."],"tags":["cancellation","async","download","cooperative-cancel"],"backgroundTag":"operation-cancelled","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}