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 toView on GitHub (pinned to a221052e0d)
Solutions
- 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
- Check the configured backend URL source (env var / TOML config) for a missing scheme, empty value, or placeholder text
- Validate the URL at startup before building any client, so the failure names the config key rather than surfacing deep in a request
- 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
- Fail fast at startup: validate the backend URL config once and name the config key in the error
- Never build the base by string concatenation of optional parts; store the full absolute URL
- Add a config-example entry showing the scheme explicitly (https://...) so copies keep it
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
- Endpoint must start with http:// or https://
- paths must be repo-relative, not absolute (got "${p}")
- must be an array
- RPC ${method} returned non-JSON HTTP ${res.status}: ${bodyTe
- provider is required
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/a16b79e64a742395.
Report an issue: GitHub.