{"record":{"id":"79c2330b70c9ae4f","repo":"astrid-runtime/astrid","slug":"endpoint-is-not-an-http-s-url-url","errorCode":null,"errorMessage":"endpoint is not an http(s) URL: {url}","messagePattern":"endpoint is not an http\\(s\\) URL: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/capsule/model_discovery.rs","lineNumber":190,"sourceCode":"/// sent only when `bearer` resolves to a non-empty value after trimming\n/// **and** the resolved fetch host matches the host of the user-configured\n/// provider `base_url` (see [`should_send_bearer`]) — so a capsule cannot\n/// exfiltrate the credential to an arbitrary host. The response body is\n/// capped at [`MAX_RESPONSE_BYTES`].\n///\n/// Returns `Ok(non_empty_options)` on success, or `Err` on any failure\n/// (unresolved template, network error, non-2xx, oversized body, non-JSON,\n/// empty list). The caller maps `Err` to a free-text fallback.\npub(crate) async fn fetch_options(\n    opts: &OptionsFrom,\n    values: &HashMap<String, String>,\n) -> anyhow::Result<Vec<String>> {\n    let url = resolve_template(&opts.http, values);\n    anyhow::ensure!(\n        !url.contains('{'),\n        \"endpoint still contains unresolved placeholders: {url}\"\n    );\n    anyhow::ensure!(\n        url.starts_with(\"http://\") || url.starts_with(\"https://\"),\n        \"endpoint is not an http(s) URL: {url}\"\n    );\n\n    let bearer = opts\n        .bearer\n        .as_ref()\n        .map(|b| resolve_template(b, values))\n        .map(|b| b.trim().to_string())\n        .filter(|b| !b.is_empty());\n\n    // Bind the credential to the configured provider host. A capsule's\n    // `http`/`bearer` are independent templates with no inherent host\n    // binding, so without this check a manifest could point `http` at an\n    // attacker host while still resolving `bearer` to the user's API key.\n    // The bearer is attached only when the resolved fetch host matches the\n    // host of the user-configured `base_url`; otherwise it is withheld (the\n    // request will most likely 401 and fall back to free-text — the correct","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/capsule/model_discovery.rs#L172-L208","documentation":"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.","triggerScenarios":"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.","commonSituations":"Misconfigured capsule endpoint missing the scheme prefix; config authored for a different tool expecting relative URLs; typos dropping the `https://` prefix.","solutions":["Prefix the endpoint in the capsule config with https:// or http://","Fix the endpoint template/value so the resolved URL is absolute with an http(s) scheme","Validate the configured endpoint when writing capsule config"],"exampleFix":"// before\nendpoint = \"myhost.example/v1/models\"\n// after\nendpoint = \"https://myhost.example/v1/models\"","handlingStrategy":"validation","validationCode":"if !(url.starts_with(\"http://\") || url.starts_with(\"https://\")) {\n    eprintln!(\"endpoint must be an absolute http(s) URL: {}\", url);\n    return Ok(());\n}","typeGuard":null,"tryCatchPattern":"match fetch_options(opts, values).await {\n    Err(e) if e.to_string().contains(\"not an http(s) URL\") => prompt_free_text()?,\n    other => other?,\n}","preventionTips":["Always author capsule endpoints with an explicit https:// (or http:// for local) prefix","Validate endpoint config at capsule packaging time","Avoid relative paths or bare hostnames in endpoint templates"],"tags":["url","scheme","validation"],"backgroundTag":"invalid-url-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}