{"record":{"id":"a16b79e64a742395","repo":"tinyhumansai/openhuman","slug":"api-base-url-must-be-an-absolute-http-s-url-with","errorCode":null,"errorMessage":"API base URL must be an absolute http(s) URL with host","messagePattern":"API base URL must be an absolute http\\(s\\) URL with host","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":415,"sourceCode":"#[derive(Clone)]\npub struct BackendOAuthClient {\n    client: Client,\n    base: Url,\n    sdk: TinyHumansClient,\n}\n\nimpl BackendOAuthClient {\n    /// Creates a new `BackendOAuthClient` with the given API base URL.\n    ///\n    /// Any path, query, or fragment in `api_base` is stripped so that\n    /// `Url::join` always resolves root-relative REST paths correctly.\n    /// This guards against callers who pass a full LLM completions URL\n    /// (e.g. `https://host/v1/chat/completions`) instead of just the origin:\n    /// without stripping, `join(\"teams/me/usage\")` would produce the wrong\n    /// path `/v1/chat/teams/me/usage` via RFC 3986 relative resolution.\n    pub fn new(api_base: &str) -> Result<Self> {\n        let mut base = Url::parse(api_base.trim()).context(\"Invalid API base URL\")?;\n        anyhow::ensure!(\n            matches!(base.scheme(), \"http\" | \"https\") && base.host_str().is_some(),\n            \"API base URL must be an absolute http(s) URL with host\"\n        );\n        base.set_path(\"\");\n        base.set_query(None);\n        base.set_fragment(None);\n        let client = build_backend_reqwest_client()?;\n        // The product identity also rides on the SDK's own default headers, not\n        // just the transport's, so it survives if the SDK is ever given a\n        // client this crate did not build. The SDK applies its own headers\n        // after these, so it cannot be clobbered by `x-sdk-client`.\n        let sdk = TinyHumansClient::new(base.as_str())\n            .with_http_client(client.clone())\n            .with_default_headers(crate::api::product::product_identity_headers());\n        Ok(Self { client, base, sdk })\n    }\n\n    /// Borrow the underlying `reqwest::Client` for callers that need to","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L397-L433","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet client = BackendOAuthClient::new(cfg.backend_api_url.as_str())?;\n// cfg.backend_api_url = \"api.tinyhumans.ai\" -> Invalid API base URL / scheme failure\n\n// after\nlet base = cfg.backend_api_url.trim();\nlet base = if base.starts_with(\"http://\") || base.starts_with(\"https://\") {\n    base.to_string()\n} else {\n    format!(\"https://{base}\")\n};\nlet client = BackendOAuthClient::new(&base)?;","handlingStrategy":"validation","validationCode":"fn valid_backend_base(url: &str) -> bool {\n    match url::Url::parse(url.trim()) {\n        Ok(u) => matches!(u.scheme(), \"http\" | \"https\") && u.host_str().is_some(),\n        Err(_) => false,\n    }\n}\n\n// before constructing:\nassert!(valid_backend_base(&cfg.backend_api_url),\n    \"backend_api_url must be absolute http(s) with host, got {:?}\", cfg.backend_api_url);","typeGuard":"fn normalize_backend_base(raw: &str) -> Option<String> {\n    let s = raw.trim();\n    let s = if s.starts_with(\"http://\") || s.starts_with(\"https://\") {\n        s.to_string()\n    } else if s.contains(\"://\") {\n        return None; // non-http scheme, do not guess\n    } else if s.is_empty() {\n        return None;\n    } else {\n        format!(\"https://{s}\")\n    };\n    valid_backend_base(&s).then_some(s)\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","config","url","validation","backend-api"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}