aaif-goose/goose · error

Failed to construct URL: {}

Error message

Failed to construct URL: {}

What it means

After the base URL parsed successfully, base_url.join(path) failed while resolving the request path against it. In practice this only happens when the base URL is a 'cannot-be-a-base' URL (no hierarchical path, e.g. 'data:...', 'mailto:...', or a URL like 'https://host#frag' edge cases) or the path argument contains characters/structure that cannot combine into a valid URL.

Source

Thrown at crates/goose-providers/src/api_client.rs:466

    }

    pub async fn response_get(&self, path: &str) -> Result<Response> {
        self.request(path).response_get().await
    }

    fn build_url(&self, path: &str) -> Result<url::Url> {
        use url::Url;
        let mut base_url =
            Url::parse(&self.host).map_err(|e| anyhow::anyhow!("Invalid base URL: {}", e))?;

        let base_path = base_url.path();
        if !base_path.is_empty() && base_path != "/" && !base_path.ends_with('/') {
            base_url.set_path(&format!("{}/", base_path));
        }

        let mut url = base_url
            .join(path)
            .map_err(|e| anyhow::anyhow!("Failed to construct URL: {}", e))?;

        for (key, value) in &self.default_query {
            url.query_pairs_mut().append_pair(key, value);
        }

        Ok(url)
    }
}

impl<'a> ApiRequestBuilder<'a> {
    pub fn header(mut self, key: &str, value: &str) -> Result<Self> {
        let header_name = HeaderName::from_bytes(key.as_bytes())?;
        let header_value = HeaderValue::from_str(value)?;
        self.headers.insert(header_name, header_value);
        Ok(self)
    }

    #[allow(dead_code)]

View on GitHub (pinned to 3810898a74)

Solutions

  1. Use an http:// or https:// base URL with a normal host and path
  2. URL-encode path segments coming from user input (percent-encode spaces and special characters)
  3. If path is sometimes an absolute URL, branch: parse it directly instead of joining it onto the base

Example fix

// before
let client = ApiClient::new("data:text/plain,hello".into(), ...);
client.api_get("models"); // Failed to construct URL

// after
let client = ApiClient::new("https://api.example.com/v1/".into(), ...);
client.api_get("models");
Defensive patterns

Strategy: try-catch

Validate before calling

fn url_join_ok(host: &str, path: &str) -> bool {
    url::Url::parse(host).ok().and_then(|b| b.join(path).ok()).is_some()
}

Try / catch

match client.api_get(path).await {
    Err(e) if e.to_string().contains("Failed to construct URL") => {
        // path is bad, not the network: sanitize segments or reject the input
        let safe: String = path.chars().map(|c| if c.is_control() || c == ' ' { '_' } else { c }).collect();
        client.api_get(&safe).await
    }
    r => r,
}

Prevention

When it happens

Trigger: Constructing an ApiClient with a scheme that has no authority/path structure (data:, mailto:) so join() has nothing to resolve against, or passing a request path with invalid characters (spaces, unescaped '%' sequences) that break URL composition.

Common situations: Redirect-following or dynamically discovered endpoints feed a fully-formed absolute URL or a blob/data URI into a field expected to be an http(s) base; paths built from unencoded user input containing spaces or stray '%' signs.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/aefeac3bcf507aa3. Report an issue: GitHub.