astrid-runtime/astrid · error

endpoint still contains unresolved placeholders

Error message

endpoint still contains unresolved placeholders: {url}

What it means

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.

Solutions

  1. Provide a value for every placeholder in the endpoint template before calling discovery
  2. Fix the placeholder name in the capsule config so it matches an available values key
  3. Make the placeholder optional in the template or hardcode the value in the endpoint

Example fix

// before
values: {}  // template: http://host/v1/{model}/models
// after
values: {"model": "llama3"} // resolves to http://host/v1/llama3/models
Defensive patterns

Strategy: validation

Validate before calling

let url = resolve_template(&opts.http, &values);
if url.contains('{') {
    eprintln!("endpoint template unresolved: {}", url);
    return Ok(()); // skip discovery, use free-text
}

Try / catch

match fetch_options(opts, values).await {
    Err(e) if e.to_string().contains("unresolved placeholders") => prompt_free_text()?,
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/bec574635f98188a. Report an issue: GitHub.

Appendix: source

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

/// Resolve the live option list for a dynamic-select field.
///
/// Substitutes `values` into the `http`/`bearer` templates, performs a
/// `GET`, and parses the response. The `Authorization: Bearer` header is
/// 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

View on GitHub (pinned to affd8760f4)