{"record":{"id":"c5733dfbf0b2dbeb","repo":"Hmbown/CodeWhale","slug":"custom-provider-base-url-is-required","errorCode":null,"errorMessage":"custom provider base URL is required","messagePattern":"custom provider base URL is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/config_persistence.rs","lineNumber":712,"sourceCode":"    }\n    if crate::config::ApiProvider::parse(value).is_some() {\n        bail!(\"custom provider name must not shadow a built-in provider\");\n    }\n    if !value\n        .chars()\n        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-'))\n    {\n        bail!(\"custom provider name may only use letters, numbers, '-' and '_'\");\n    }\n    Ok(value.to_string())\n}\n\nfn normalize_custom_provider_base_url(raw: &str) -> anyhow::Result<String> {\n    use anyhow::bail;\n\n    let value = raw.trim().trim_end_matches('/');\n    if value.is_empty() {\n        bail!(\"custom provider base URL is required\");\n    }\n    let parsed = reqwest::Url::parse(value)\n        .map_err(|err| anyhow::anyhow!(\"custom provider base URL is invalid: {err}\"))?;\n    if !matches!(parsed.scheme(), \"http\" | \"https\") || parsed.host_str().is_none() {\n        bail!(\"custom provider base URL must be an http(s) URL with a host\");\n    }\n    Ok(value.to_string())\n}\n\nfn normalize_optional_custom_provider_field(raw: &str) -> Option<String> {\n    let value = raw.trim();\n    (!value.is_empty()).then(|| value.to_string())\n}\n\npub(crate) fn persist_hotbar_bindings(\n    config_path: Option<&Path>,\n    bindings: &[codewhale_config::HotbarBindingToml],\n) -> anyhow::Result<PathBuf> {","sourceCodeStart":694,"sourceCodeEnd":730,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/config_persistence.rs#L694-L730","documentation":"normalize_custom_provider_base_url rejects an empty base URL after trimming and trailing-slash removal; persist_custom_provider requires every custom provider to carry a non-empty endpoint before it is written to config.","triggerScenarios":"Calling persist_custom_provider with an empty or whitespace-only base_url string.","commonSituations":"A form submitted before the URL field was filled; an env var or config value that expanded to empty.","solutions":["Provide the provider's API endpoint (e.g. \"https://api.example.com/v1\") before persisting.","Require the URL field in the caller/UI before submit."],"exampleFix":"// before\npersist_custom_provider(path, \"my_gateway\", \"\", ...)?;\n// after\npersist_custom_provider(path, \"my_gateway\", \"https://api.example.com/v1\", ...)?;","handlingStrategy":"validation","validationCode":"if url.trim().is_empty() { return Err(anyhow::anyhow!(\"base URL required\")); }","typeGuard":"fn has_base_url(raw: &str) -> bool { !raw.trim().trim_end_matches('/').is_empty() }","tryCatchPattern":"match persist_custom_provider(path, name, &url, ...) {\n    Err(e) if e.to_string().contains(\"base URL is required\") => show_form_error(\"URL required\"),\n    other => other,\n}","preventionTips":["Make the base URL field required in forms.","Default the URL from a template like https://host/v1."],"tags":["validation","custom-provider","url","empty-value"],"backgroundTag":"empty-required-field","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}