tinyhumansai/openhuman · error

API base URL must be an absolute http(s) URL with host

Error message

API base URL must be an absolute http(s) URL with host

What it means

Thrown by BackendOAuthClient::new when the api_base string parses as a URL but is not an absolute http(s) URL with a host. Url::parse must succeed first (a scheme-less string like "api.example.com" fails earlier with "Invalid API base URL"), then the guard rejects any scheme other than http/https and any host-less URL. Paths, queries and fragments are stripped right after, so passing a full endpoint URL is fine — only the scheme+host pair is mandatory.

Source

Thrown at src/api/rest.rs:415

#[derive(Clone)]
pub struct BackendOAuthClient {
    client: Client,
    base: Url,
    sdk: TinyHumansClient,
}

impl BackendOAuthClient {
    /// Creates a new `BackendOAuthClient` with the given API base URL.
    ///
    /// Any path, query, or fragment in `api_base` is stripped so that
    /// `Url::join` always resolves root-relative REST paths correctly.
    /// This guards against callers who pass a full LLM completions URL
    /// (e.g. `https://host/v1/chat/completions`) instead of just the origin:
    /// without stripping, `join("teams/me/usage")` would produce the wrong
    /// path `/v1/chat/teams/me/usage` via RFC 3986 relative resolution.
    pub fn new(api_base: &str) -> Result<Self> {
        let mut base = Url::parse(api_base.trim()).context("Invalid API base URL")?;
        anyhow::ensure!(
            matches!(base.scheme(), "http" | "https") && base.host_str().is_some(),
            "API base URL must be an absolute http(s) URL with host"
        );
        base.set_path("");
        base.set_query(None);
        base.set_fragment(None);
        let client = build_backend_reqwest_client()?;
        // The product identity also rides on the SDK's own default headers, not
        // just the transport's, so it survives if the SDK is ever given a
        // client this crate did not build. The SDK applies its own headers
        // after these, so it cannot be clobbered by `x-sdk-client`.
        let sdk = TinyHumansClient::new(base.as_str())
            .with_http_client(client.clone())
            .with_default_headers(crate::api::product::product_identity_headers());
        Ok(Self { client, base, sdk })
    }

    /// Borrow the underlying `reqwest::Client` for callers that need to

View on GitHub (pinned to a221052e0d)

Solutions

  1. Pass an absolute URL including scheme and host, e.g. "https://api.example.com" — path/query are stripped automatically so the origin alone is ideal
  2. Check the configured backend URL source (env var / TOML config) for a missing scheme, empty value, or placeholder text
  3. Validate the URL at startup before building any client, so the failure names the config key rather than surfacing deep in a request
  4. If the value comes from user input, normalize it (default scheme, trim) before construction

Example fix

// before
let client = BackendOAuthClient::new(cfg.backend_api_url.as_str())?;
// cfg.backend_api_url = "api.tinyhumans.ai" -> Invalid API base URL / scheme failure

// after
let base = cfg.backend_api_url.trim();
let base = if base.starts_with("http://") || base.starts_with("https://") {
    base.to_string()
} else {
    format!("https://{base}")
};
let client = BackendOAuthClient::new(&base)?;
Defensive patterns

Strategy: validation

Validate before calling

fn valid_backend_base(url: &str) -> bool {
    match url::Url::parse(url.trim()) {
        Ok(u) => matches!(u.scheme(), "http" | "https") && u.host_str().is_some(),
        Err(_) => false,
    }
}

// before constructing:
assert!(valid_backend_base(&cfg.backend_api_url),
    "backend_api_url must be absolute http(s) with host, got {:?}", cfg.backend_api_url);

Type guard

fn normalize_backend_base(raw: &str) -> Option<String> {
    let s = raw.trim();
    let s = if s.starts_with("http://") || s.starts_with("https://") {
        s.to_string()
    } else if s.contains("://") {
        return None; // non-http scheme, do not guess
    } else if s.is_empty() {
        return None;
    } else {
        format!("https://{s}")
    };
    valid_backend_base(&s).then_some(s)
}

Prevention

When it happens

Trigger: Constructing BackendOAuthClient with "http://" (no host), "ftp://host" or "file:///tmp" (wrong scheme), or a value that only becomes host-less after parsing. Note "localhost:3000" and "api.example.com" never reach this bail — Url::parse rejects them first with the "Invalid API base URL" context.

Common situations: The backend URL env/config value lost its "https://" prefix, a localhost dev URL was written without a scheme, someone swapped in a ws:// or custom-scheme endpoint, or an empty string / placeholder was left in config. Also happens when the base is assembled by string concat and one fragment is empty.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/a16b79e64a742395. Report an issue: GitHub.