{"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":721,"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":703,"sourceCodeEnd":739,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/model_catalog.rs#L703-L739","documentation":"fetch_relay_profile_model_ids builds a request source from the RelayProfile, preferring an override base URL but otherwise using upstream_base_url. If the resulting trimmed base_url is empty, no models endpoint can be constructed, so it bails before any network call.","triggerScenarios":"Calling fetch_relay_profile_model_ids with a profile whose upstream_base_url (and any override) is empty or whitespace-only.","commonSituations":"Profile created but upstream URL never filled in; URL field cleared in the manager UI; profile loaded from an old/partial config file missing the field.","solutions":["Set upstream_base_url on the RelayProfile (e.g. https://api.example.com) before fetching.","If the UI is the source, require the Base URL field before enabling the 'fetch models' action.","Trim-check profile.upstream_base_url client-side and show a validation error instead of calling.","Re-save the profile ensuring the base URL persisted."],"exampleFix":"// before\nlet ids = fetch_relay_profile_model_ids(&profile).await?; // profile.upstream_base_url == \"\"\n// after\nassert!(!profile.upstream_base_url.trim().is_empty());\nlet ids = fetch_relay_profile_model_ids(&profile).await?;","handlingStrategy":"validation","validationCode":"fn can_fetch_models(profile: &RelayProfile) -> bool {\n    !profile.upstream_base_url.trim().is_empty()\n}","typeGuard":null,"tryCatchPattern":"if profile.upstream_base_url.trim().is_empty() {\n    ui.require_field(\"Base URL\");\n} else {\n    match fetch_relay_profile_model_ids(&profile).await { /* ... */ }\n}","preventionTips":["Make Base URL a required field in the profile form","Disable fetch-models button until base URL is non-empty","Validate on profile save, not just at fetch time","Trim and normalize the URL when persisting the profile"],"tags":["validation","config","http","url"],"backgroundTag":"missing-required-config-field","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}