{"record":{"id":"7c341c04eb03e64d","repo":"tonhowtf/omniget","slug":"download-cancelled-direct-downloader","errorCode":null,"errorMessage":"Download cancelled","messagePattern":"Download cancelled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"src-tauri/omniget-core/src/core/direct_downloader.rs","lineNumber":80,"sourceCode":"    progress_tx: mpsc::Sender<ProgressUpdate>,\n    headers: Option<reqwest::header::HeaderMap>,\n    cancel: Option<&CancellationToken>,\n) -> anyhow::Result<u64> {\n    let mut last_err = None;\n    // Two independent budgets, both monotonic, so the loop always terminates:\n    // `attempt` counts the ordinary retries and `forbidden_retries` counts the\n    // 403 ladder. The ladder gets its own counter so a couple of transient\n    // network failures cannot eat the escalation steps before they run, and it\n    // is a local — the count is per download request, never process-wide.\n    let mut attempt: u32 = 0;\n    let mut forbidden_retries: u32 = 0;\n    let mut requests_made: u32 = 0;\n    let mut effective_headers = headers;\n\n    while attempt < MAX_RETRIES {\n        if let Some(token) = cancel {\n            if token.is_cancelled() {\n                return Err(anyhow!(\"Download cancelled\"));\n            }\n        }\n\n        if requests_made > 0 {\n            let base = 1000 * (requests_made as u64);\n            let jitter = rand::random::<u64>() % (base / 2 + 1);\n            tokio::time::sleep(Duration::from_millis(base + jitter)).await;\n        }\n\n        requests_made += 1;\n        match download_attempt(\n            client,\n            url,\n            output,\n            &progress_tx,\n            effective_headers.clone(),\n            cancel,\n        )","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/direct_downloader.rs#L62-L98","documentation":"download_direct_with_headers checks the caller-supplied CancellationToken at the top of each retry attempt and aborts immediately with this error when the token is already cancelled. It is the library's cooperative-cancellation mechanism for direct (non-aria2c) downloads, including mid-download retries.","triggerScenarios":"The caller cancels the CancellationTokenSource (user pressed stop, UI closed, timeout policy) while download_direct_with_headers is between attempts or before the first request; on the next loop iteration token.is_cancelled() returns true and this error is returned.","commonSituations":"User cancels a file download in the app UI; a supervisor cancels the token after a global timeout; app shutdown cancels in-flight downloads; retry loop observes cancellation before issuing the next retry request.","solutions":["Treat this error as an expected, non-fatal signal: stop and clean up without alerting the user","Check token.is_cancelled() before calling download_direct to skip no-op work","Use tokio::select! on the download future vs token.cancelled() for immediate cancellation even mid-request","If cancellation was unintentional, verify the token lifecycle — tokens are not resettable; create a fresh CTS per download"],"exampleFix":"// before\nmatch download_direct(url, Some(&token)).await {\n    Err(e) => return Err(e), // treats cancel like a hard failure\n    Ok(f) => f,\n}\n// after\nmatch download_direct(url, Some(&token)).await {\n    Err(e) if format!(\"{}\", e).contains(\"Download cancelled\") => {\n        tracing::info!(\"download cancelled by user\"); // expected path\n    }\n    Err(e) => return Err(e),\n    Ok(f) => f,\n}","handlingStrategy":"try-catch","validationCode":"// Check before invoking to avoid a guaranteed error return\nif token.as_ref().map(|t| t.is_cancelled()).unwrap_or(false) {\n    eprintln!(\"download already cancelled; skipping call\");\n    return;\n}","typeGuard":null,"tryCatchPattern":"tokio::select! {\n    res = download_direct_with_headers(url, headers, Some(&token)) => match res {\n        Err(e) if e.to_string().contains(\"Download cancelled\") => {\n            tracing::info!(\"download cancelled by user\"); // expected, non-fatal\n        }\n        other => other?,\n    },\n    _ = token.cancelled() => tracing::info!(\"cancelled via token\"),\n}","preventionTips":["Handle this error as a control-flow signal, not a failure — never report it as a crash","Use tokio::select! with token.cancelled() to react to cancellation promptly, even mid-request","Create a fresh CancellationTokenSource per download; tokens cannot be reset once cancelled","Check the token before starting and between retry attempts to avoid wasted work","Clean up partial files on cancellation so retries start from a clean state"],"tags":["rust","cancellation","download","control-flow"],"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"}