{"record":{"id":"bec574635f98188a","repo":"astrid-runtime/astrid","slug":"endpoint-still-contains-unresolved-placeholders","errorCode":null,"errorMessage":"endpoint still contains unresolved placeholders: {url}","messagePattern":"endpoint still contains unresolved placeholders: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/capsule/model_discovery.rs","lineNumber":186,"sourceCode":"/// Resolve the live option list for a dynamic-select field.\n///\n/// Substitutes `values` into the `http`/`bearer` templates, performs a\n/// `GET`, and parses the response. The `Authorization: Bearer` header is\n/// 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","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/capsule/model_discovery.rs#L168-L204","documentation":"fetch_options resolves the model endpoint URL template with the user's form values and then rejects it before any network call if it still contains `{` placeholders. This prevents sending requests to malformed operator-supplied endpoints.","triggerScenarios":"The capsule's endpoint template contains a placeholder (e.g. `{model_id}`) that the user-supplied values map does not fill in, so resolve_template leaves `{...}` in the URL.","commonSituations":"Capsule config defines a template placeholder but the discovery options form has no matching field, or the user skipped/blanked the field; typo between template variable name and values key.","solutions":["Provide a value for every placeholder in the endpoint template before calling discovery","Fix the placeholder name in the capsule config so it matches an available values key","Make the placeholder optional in the template or hardcode the value in the endpoint"],"exampleFix":"// before\nvalues: {}  // template: http://host/v1/{model}/models\n// after\nvalues: {\"model\": \"llama3\"} // resolves to http://host/v1/llama3/models","handlingStrategy":"validation","validationCode":"let url = resolve_template(&opts.http, &values);\nif url.contains('{') {\n    eprintln!(\"endpoint template unresolved: {}\", url);\n    return Ok(()); // skip discovery, use free-text\n}","typeGuard":null,"tryCatchPattern":"match fetch_options(opts, values).await {\n    Err(e) if e.to_string().contains(\"unresolved placeholders\") => prompt_free_text()?,\n    other => other?,\n}","preventionTips":["Ensure every `{placeholder}` in the endpoint template has a matching key in the values map","Collect all required form fields before invoking discovery","Test capsule endpoint templates with sample values before publishing"],"tags":["url","template","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"}