{"record":{"id":"f2511181f389f043","repo":"Hmbown/CodeWhale","slug":"download-url-returned-status","errorCode":null,"errorMessage":"download {url} returned {status}","messagePattern":"download (.+?) returned (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/install.rs","lineNumber":1156,"sourceCode":"    Bytes(Vec<u8>),\n    NotFound(reqwest::StatusCode),\n}\n\n/// Stream a URL into memory with a size cap. Aborts on the first read that\n/// would push the buffer over `max_size * 4` (the *4 accounts for compression;\n/// the unpack step still enforces `max_size` on the *uncompressed* bytes).\nasync fn download_with_cap(url: &str, max_size: u64) -> Result<DownloadAttempt> {\n    let resp = reqwest_client()\n        .get(url)\n        .send()\n        .await\n        .with_context(|| format!(\"failed to GET {url}\"))?;\n    let status = resp.status();\n    if !status.is_success() {\n        if status == reqwest::StatusCode::NOT_FOUND {\n            return Ok(DownloadAttempt::NotFound(status));\n        }\n        bail!(\"download {url} returned {status}\");\n    }\n    // Soft cap on the *compressed* download — well above max_size to allow\n    // for highly compressible payloads but still bounded.\n    let compressed_cap = max_size.saturating_mul(4);\n    let bytes = resp\n        .bytes()\n        .await\n        .with_context(|| format!(\"failed to read body of {url}\"))?;\n    if (bytes.len() as u64) > compressed_cap {\n        bail!(\"download {url} exceeds compressed size cap of {compressed_cap} bytes\");\n    }\n    Ok(DownloadAttempt::Bytes(bytes.to_vec()))\n}\n\nstruct StagedSkill {\n    skill_name: String,\n    staged_path: PathBuf,\n}","sourceCodeStart":1138,"sourceCodeEnd":1174,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/install.rs#L1138-L1174","documentation":"Raised by download_with_cap for a single URL that answered with a non-success status other than 404. 404 is swallowed as NotFound so the candidate loop can try alternates; every other error status (403, 401, 5xx) bails here with the URL and status in the message. Because the bail propagates through '?' in the candidate loop, remaining candidate URLs are not tried after it, unlike the 404 path.","triggerScenarios":"A direct-URL install pointing at a permission-gated artifact (403), an auth-required or expired signed URL (401), or a server error (500/502). With the github: shorthand, a non-404 error on the main-branch archive aborts before the master fallback is tried.","commonSituations":"Private or auth-required artifacts, rate-limited CDNs returning 403, expired signed URLs, and mirrors returning 502.","solutions":["Act on the status: 401/403 means the artifact needs credentials or is private; 5xx means retry later.","Fix or replace the URL; a moved artifact should get its new address.","When using the github: shorthand and the first candidate errors (not 404), switch to a DirectUrl for the correct branch."],"exampleFix":"# before\n/skill install https://cdn.example.com/pack.tar.gz\n# -> download ... returned 403 Forbidden\n\n# after: artifact requires auth; use the public mirror\n/skill install https://mirror.example.com/pack.tar.gz","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for url in candidate_urls_after_failure {\n    match download_with_cap(&url, max_size).await {\n        Err(err) if err.to_string().contains(\"returned 403\") || err.to_string().contains(\"returned 401\") => {\n            continue; // auth-gated: try the next candidate URL, retrying will not help\n        }\n        Err(err) if err.to_string().contains(\"returned 5\") => {\n            backoff_and_retry(&url).await; // transient server error\n        }\n        other => return other.map(|_| ()),\n    }\n}","preventionTips":["Distinguish auth errors (401/403) from server errors (5xx): only the latter benefit from retry.","Keep signed URLs fresh; expired ones return 401 immediately.","Remember a non-404 error aborts the candidate loop, so supply fallback URLs yourself."],"tags":["skills","download","http-status","rust"],"backgroundTag":"http-error-status","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}