{"record":{"id":"60cd62a0c8db76a3","repo":"Hmbown/CodeWhale","slug":"bundle-redirects-may-not-change-url-scheme","errorCode":null,"errorMessage":"bundle redirects may not change URL scheme","messagePattern":"bundle redirects may not change URL scheme","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/cli/src/config_bundles.rs","lineNumber":833,"sourceCode":"    if !matches!(url.scheme(), \"http\" | \"https\") {\n        bail!(\"unsupported bundle URL scheme; use https\");\n    }\n    if !url.username().is_empty() || url.password().is_some() {\n        bail!(\"bundle URLs may not include credentials\");\n    }\n    let host = url.host_str().context(\"bundle URL must include a host\")?;\n    match url.scheme() {\n        \"https\" => Ok(()),\n        \"http\" if is_loopback_bundle_host(host) => Ok(()),\n        \"http\" => bail!(\"plain http is only allowed for loopback hosts; use https\"),\n        _ => unreachable!(\"scheme was validated above\"),\n    }\n}\n\nfn validate_bundle_redirect(initial_scheme: &str, next_url: &reqwest::Url) -> Result<()> {\n    validate_bundle_url(next_url)?;\n    if next_url.scheme() != initial_scheme {\n        bail!(\"bundle redirects may not change URL scheme\");\n    }\n    Ok(())\n}\n\nfn is_loopback_bundle_host(host: &str) -> bool {\n    let normalized = host\n        .strip_prefix('[')\n        .and_then(|value| value.strip_suffix(']'))\n        .unwrap_or(host);\n    normalized.eq_ignore_ascii_case(\"localhost\")\n        || normalized.to_ascii_lowercase().ends_with(\".localhost\")\n        || normalized\n            .parse::<std::net::IpAddr>()\n            .is_ok_and(|address| address.is_loopback())\n}\n\n// ---------------------------------------------------------------------------\n// Export","sourceCodeStart":815,"sourceCodeEnd":851,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/cli/src/config_bundles.rs#L815-L851","documentation":"validate_bundle_redirect enforces that an HTTP redirect during a bundle fetch does not change the URL scheme (e.g. https → http, or http → https crossing onto the wrong class of host). Scheme changes mid-fetch would let a redirect silently downgrade security (TLS to plaintext) or bypass the loopback-only-http rule, so the fetch is aborted.","triggerScenarios":"A bundle server responds with 301/302/307/308 whose Location header targets a different scheme than the originally requested scheme; fetch_bundle validates each hop with validate_bundle_redirect, which also validates the redirect target itself.","commonSituations":"An https bundle URL that redirects to an http mirror; a load balancer redirecting http→https (fails if the client originally used plain http on a non-loopback target anyway); a CDN rewriting to a different scheme.","solutions":["Point the bundle URL directly at the final destination so no redirect occurs.","Configure the server to keep redirects scheme-preserving (https→https, http-loopback→http-loopback).","Have ops fix the redirect chain so the https location does not downgrade to http.","Test the redirect chain with `curl -IL <url>` and correct the Location header on the server."],"exampleFix":"// before (server config)\n# server: redirect https://a/bundle -> http://mirror/bundle\n// after\n# redirect https://a/bundle -> https://mirror/bundle  (scheme preserved)","handlingStrategy":"validation","validationCode":"// Before configuring a bundle URL, check the redirect chain:\n// curl -sIL -o /dev/null -w '%{url_effective} %{scheme}\\n' <bundle-url>\n// Ensure every hop keeps the same scheme.","typeGuard":"fn redirect_is_scheme_preserving(initial: &str, next: &reqwest::Url) -> bool {\n    next.scheme() == initial\n}","tryCatchPattern":null,"preventionTips":["Point bundle URLs at the final destination to avoid redirects entirely.","Audit server redirect rules so they never downgrade https to http.","Periodically run curl -IL against bundle endpoints in CI to detect scheme-changing redirects."],"tags":["network","security","redirect","http","config-bundles"],"backgroundTag":"http-error-response","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}