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
- 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
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
- 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
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
- capsule ' ' env. references undefined variable ' }}}}
- endpoint is not an http(s) URL
- a corpus produced no chunks
- a representation record must cover at least one logical…
- absent migration source has a digest
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 anView on GitHub (pinned to affd8760f4)