Hmbown/CodeWhale · error

invalid registry url

Error message

invalid registry url: {registry_url}

What it means

`fetch_registry` parses the registry URL to extract a host for the network-policy decision. If `host_from_url` returns `None` the string is not a usable absolute URL and no policy decision can be made, so it fails fast with an invalid-URL error before any network activity.

Solutions

  1. Use a full absolute URL with scheme, e.g. `https://registry.example.com/skills.json`
  2. Check the registry URL config/env value for truncation or missing scheme
  3. Trim whitespace before passing

Example fix

// before
fetch_registry(&network, "my-registry.example.com").await?;
// after
fetch_registry(&network, "https://my-registry.example.com/skills.json").await?;
Defensive patterns

Strategy: validation

Validate before calling

fn valid_registry_url(u: &str) -> bool {
    url::Url::parse(u).map(|p| p.host_str().is_some() && matches!(p.scheme(), "http" | "https")).unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling `fetch_registry(network, registry_url)` (directly or via `sync_registry`/`candidate_urls`) with a string like `"my-registry"`, `"ftp://x"`-style unsupported schemes, empty string, or a URL missing a scheme/host.

Common situations: Config file with a bare hostname instead of `https://host/registry.json`; environment variable left empty; typos like `https:/host` (single slash); copy-paste dropping the scheme.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/055d5a846fccb300. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/skills/install.rs:550

    if !target.join(INSTALLED_FROM_MARKER).exists() {
        return Err(InstallError::NotInstalledHere(name.to_string()).into());
    }
    let content_digest = super::package_digest::compute_package_digest(&target)
        .with_context(|| format!("cannot compute content digest for {}", target.display()))?;
    write_trust_v2(&target, &content_digest)?;
    Ok(())
}

/// Fetch the curated registry and return the parsed entries.
///
/// Honours `network` (skipping the call entirely on Deny / Prompt).
pub async fn fetch_registry(
    network: &NetworkPolicy,
    registry_url: &str,
) -> Result<RegistryFetchResult> {
    let host = match host_from_url(registry_url) {
        Some(host) => host,
        None => bail!("invalid registry url: {registry_url}"),
    };
    match network.decide(&host) {
        Decision::Allow => {}
        Decision::Deny => return Ok(RegistryFetchResult::Denied(host)),
        Decision::Prompt => return Ok(RegistryFetchResult::NeedsApproval(host)),
    }
    let body = reqwest_client()
        .get(registry_url)
        .send()
        .await
        .with_context(|| format!("failed to fetch registry {registry_url}"))?
        .error_for_status()
        .with_context(|| format!("registry {registry_url} returned an error status"))?
        .text()
        .await
        .with_context(|| format!("failed to read registry body from {registry_url}"))?;
    let parsed: RegistryDocument = serde_json::from_str(&body)
        .with_context(|| format!("failed to parse registry json from {registry_url}"))?;

View on GitHub (pinned to 73e0f67d83)