{"record":{"id":"8df450eaa6c5d2ba","repo":"Hmbown/CodeWhale","slug":"invalid-download-url-url","errorCode":null,"errorMessage":"invalid download url: {url}","messagePattern":"invalid download url: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/install.rs","lineNumber":1097,"sourceCode":"        }\n    }\n}\n\n/// Download the first URL whose host the policy allows and which returns 2xx.\n/// Returns `NeedsApproval` if every candidate hit `Prompt`, or `Denied` if every\n/// candidate was denied.\nasync fn download_first_success(\n    urls: &[String],\n    network: &NetworkPolicy,\n    max_size: u64,\n) -> Result<DownloadOutcome> {\n    let mut last_status: Option<reqwest::StatusCode> = None;\n    let mut prompt_host: Option<String> = None;\n    let mut denied_host: Option<String> = None;\n    for url in urls {\n        let host = match host_from_url(url) {\n            Some(h) => h,\n            None => bail!(\"invalid download url: {url}\"),\n        };\n        match network.decide(&host) {\n            Decision::Allow => {}\n            Decision::Deny => {\n                denied_host.get_or_insert(host);\n                continue;\n            }\n            Decision::Prompt => {\n                prompt_host.get_or_insert(host);\n                continue;\n            }\n        }\n        match download_with_cap(url, max_size).await? {\n            DownloadAttempt::Bytes(bytes) => {\n                return Ok(DownloadOutcome::Bytes {\n                    bytes,\n                    url: url.clone(),\n                });","sourceCodeStart":1079,"sourceCodeEnd":1115,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/install.rs#L1079-L1115","documentation":"download_first_success extracts a host from each candidate URL to drive the network policy decision; a URL yielding no host bails the whole loop. Only well-formed http(s) URLs with a host should ever be enqueued, so this is a guard against malformed URL construction upstream.","triggerScenarios":"A DirectUrl spec with a scheme but no host ('https:///x.tar.gz'), or an internally built GitHub archive URL degenerated by an empty repo string.","commonSituations":"Specs assembled by string formatting where a component was empty, URLs with unicode lookalike characters, and trailing-punctuation paste artifacts.","solutions":["Re-enter the source as a full, well-formed URL including the host.","Validate with url::Url::parse plus a host check before install.","If the URL was built internally from a github: spec, fix the spec; see the malformed-github-spec errors."],"exampleFix":"# before\n/skill install https:///skills/pack.tar.gz\n\n# after\n/skill install https://example.com/skills/pack.tar.gz","handlingStrategy":"validation","validationCode":"fn download_url_ok(url: &str) -> bool {\n    matches!(url::Url::parse(url), Ok(u) if u.host_str().is_some())\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Parse URLs with url::Url before storing them as install sources.","Never build download URLs by naive string concatenation of possibly-empty parts.","Reject URLs containing spaces or unicode lookalike characters at input time."],"tags":["skills","download","url","validation","rust"],"backgroundTag":"invalid-download-url","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}