astrid-runtime/astrid · error

endpoint is not an http(s) URL

Error message

endpoint is not an http(s) URL: {url}

What it means

fetch_options validates that the resolved endpoint starts with http:// or https:// before making the request. Any other scheme (relative path, ftp, file, empty string) is rejected as unsafe for the operator-supplied endpoint.

Solutions

  1. Prefix the endpoint in the capsule config with https:// or http://
  2. Fix the endpoint template/value so the resolved URL is absolute with an http(s) scheme
  3. Validate the configured endpoint when writing capsule config

Example fix

// before
endpoint = "myhost.example/v1/models"
// after
endpoint = "https://myhost.example/v1/models"
Defensive patterns

Strategy: validation

Validate before calling

if !(url.starts_with("http://") || url.starts_with("https://")) {
    eprintln!("endpoint must be an absolute http(s) URL: {}", url);
    return Ok(());
}

Try / catch

match fetch_options(opts, values).await {
    Err(e) if e.to_string().contains("not an http(s) URL") => prompt_free_text()?,
    other => other?,
}

Prevention

When it happens

Trigger: The capsule's configured endpoint resolves to a non-http(s) string — e.g. a bare hostname, relative path like /v1/models, an ftp:// or file:// URL, or an empty endpoint.

Common situations: Misconfigured capsule endpoint missing the scheme prefix; config authored for a different tool expecting relative URLs; typos dropping the `https://` prefix.

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 astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/79c2330b70c9ae4f. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/capsule/model_discovery.rs:190

/// sent only when `bearer` resolves to a non-empty value after trimming
/// **and** the resolved fetch host matches the host of the user-configured
/// provider `base_url` (see [`should_send_bearer`]) — so a capsule cannot
/// exfiltrate the credential to an arbitrary host. The response body is
/// capped at [`MAX_RESPONSE_BYTES`].
///
/// Returns `Ok(non_empty_options)` on success, or `Err` on any failure
/// (unresolved template, network error, non-2xx, oversized body, non-JSON,
/// empty list). The caller maps `Err` to a free-text fallback.
pub(crate) async fn fetch_options(
    opts: &OptionsFrom,
    values: &HashMap<String, String>,
) -> anyhow::Result<Vec<String>> {
    let url = resolve_template(&opts.http, values);
    anyhow::ensure!(
        !url.contains('{'),
        "endpoint still contains unresolved placeholders: {url}"
    );
    anyhow::ensure!(
        url.starts_with("http://") || url.starts_with("https://"),
        "endpoint is not an http(s) URL: {url}"
    );

    let bearer = opts
        .bearer
        .as_ref()
        .map(|b| resolve_template(b, values))
        .map(|b| b.trim().to_string())
        .filter(|b| !b.is_empty());

    // Bind the credential to the configured provider host. A capsule's
    // `http`/`bearer` are independent templates with no inherent host
    // binding, so without this check a manifest could point `http` at an
    // attacker host while still resolving `bearer` to the user's API key.
    // The bearer is attached only when the resolved fetch host matches the
    // host of the user-configured `base_url`; otherwise it is withheld (the
    // request will most likely 401 and fall back to free-text — the correct

View on GitHub (pinned to affd8760f4)