{"record":{"id":"fbb5791992d3d260","repo":"BigPizzaV3/CodexPlusPlus","slug":"base-url","errorCode":null,"errorMessage":"Base URL 不能为空","messagePattern":"Base URL 不能为空","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/model_catalog.rs","lineNumber":583,"sourceCode":"    profile: &RelayProfile,\n) -> anyhow::Result<(Vec<String>, String)> {\n    let source = ModelSource {\n        source_id: format!(\"relay-profile:{}\", profile.id),\n        source_type: \"relay_profile\".to_string(),\n        name: if profile.name.trim().is_empty() {\n            profile.id.clone()\n        } else {\n            profile.name.trim().to_string()\n        },\n        base_url: if profile.upstream_base_url.trim().is_empty() {\n            profile.base_url.trim().to_string()\n        } else {\n            profile.upstream_base_url.trim().to_string()\n        },\n        api_key: profile.api_key.trim().to_string(),\n    };\n    if source.base_url.is_empty() {\n        anyhow::bail!(\"Base URL 不能为空\");\n    }\n    let endpoint = models_endpoint(&source.base_url);\n    let client = crate::http_client::proxied_client(&profile.user_agent)?;\n    let (models, status) = fetch_models_from_source(&client, &source).await;\n    if models.is_empty() {\n        let message = status\n            .get(\"message\")\n            .and_then(Value::as_str)\n            .unwrap_or(\"上游没有返回可用模型\");\n        anyhow::bail!(\"{message}\");\n    }\n    Ok((models, endpoint))\n}\n\nfn preferred_responses_api_status(sources: &[Value]) -> Value {\n    let statuses = sources\n        .iter()\n        .filter_map(|source| source.get(\"responses_api\"))","sourceCodeStart":565,"sourceCodeEnd":601,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/model_catalog.rs#L565-L601","documentation":"fetch_relay_profile_model_ids (crates/codex-plus-core/src/model_catalog.rs:564) resolves a ModelSource from a RelayProfile: it prefers upstream_base_url when non-blank, otherwise base_url, both trimmed. If the chosen URL is empty it bails with 'Base URL 不能为空' before any network call. This is config validation: the profile exists but has no usable endpoint URL, so the /models fetch for the model catalog cannot even be addressed.","triggerScenarios":"Calling fetch_relay_profile_model_ids (directly or via the manager's model-list refresh for a relay profile) where the profile's upstream_base_url and base_url are both empty or whitespace-only — e.g. a newly created profile saved before its URL field was filled, or TOML/settings migration wiping the fields.","commonSituations":"UI flow letting users save a relay profile without a required URL; profiles imported/migrated from older settings versions where the field was named differently; whitespace-only URLs pasted by accident; tests constructing RelayProfile::default()-like values without setting URLs.","solutions":["Fill in a base URL: set upstream_base_url (preferred) or base_url on the relay profile before refreshing its model list","Trim and validate before calling: reject blank URLs in the UI/save path so profiles cannot persist without an endpoint","If the profile is a placeholder, remove or disable it so catalog refresh skips it","Check both fields — an empty upstream_base_url silently falls back to base_url, so both must be blank to hit this"],"exampleFix":"// before\nlet profile = RelayProfile { name: \"demo\".into(), ..Default::default() }; // no URLs\nlet (models, endpoint) = fetch_relay_profile_model_ids(&profile).await?; // bail\n\n// after\nlet profile = RelayProfile {\n    name: \"demo\".into(),\n    upstream_base_url: \"https://api.example.com\".into(),\n    ..Default::default()\n};\nlet (models, endpoint) = fetch_relay_profile_model_ids(&profile).await?;","handlingStrategy":"validation","validationCode":"fn relay_profile_has_base_url(profile: &RelayProfile) -> bool {\n    let upstream = profile.upstream_base_url.trim();\n    let base = profile.base_url.trim();\n    !upstream.is_empty() || !base.is_empty()\n}\nassert!(relay_profile_has_base_url(&profile));","typeGuard":"fn usable_base_url(profile: &RelayProfile) -> Option<&str> {\n    let upstream = profile.upstream_base_url.trim();\n    let base = profile.base_url.trim();\n    Some(if upstream.is_empty() { base } else { upstream }).filter(|u| !u.is_empty())\n}","tryCatchPattern":"match fetch_relay_profile_model_ids(&profile).await {\n    Err(e) if e.to_string().contains(\"Base URL 不能为空\") => {\n        // config bug: skip profile, surface to user in UI\n        mark_profile_invalid(&profile.id, \"missing base URL\");\n    }\n    rest => rest?,\n}","preventionTips":["Require a non-blank base URL in the profile save path/UI before persistence","Trim inputs on save; treat whitespace-only as empty","When migrating settings, verify every relay profile resolves to a non-empty effective URL"],"tags":["rust","relay-profile","config-validation","model-catalog","missing-url"],"backgroundTag":"missing-required-config","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}