{"record":{"id":"64ecae3d5fe232e2","repo":"BigPizzaV3/CodexPlusPlus","slug":"base-url-64ecae","errorCode":null,"errorMessage":"Base URL 为空","messagePattern":"Base URL 为空","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/provider_import.rs","lineNumber":228,"sourceCode":"        user_agent: String::new(),\n        sub2api_enabled: false,\n        sub2api_multiplier: String::new(),\n        model_routes: Vec::new(),\n        standard_openai_protocol: false,\n    }\n}\n\nfn normalize_request(mut request: ProviderImportRequest) -> anyhow::Result<ProviderImportRequest> {\n    request.name = request.name.trim().to_string();\n    request.base_url = request.base_url.trim().trim_end_matches('/').to_string();\n    request.api_key = request.api_key.trim().to_string();\n    request.wire_api = request.wire_api.trim().to_ascii_lowercase();\n    request.relay_mode = request.relay_mode.trim().to_ascii_lowercase();\n    if request.name.is_empty() {\n        anyhow::bail!(\"供应商名称为空\");\n    }\n    if request.base_url.is_empty() {\n        anyhow::bail!(\"Base URL 为空\");\n    }\n    if request.api_key.is_empty() {\n        anyhow::bail!(\"API Key 为空\");\n    }\n    request.config_contents = build_config_toml(\n        &request.base_url,\n        &request.api_key,\n        relay_protocol(&request.wire_api),\n    );\n    request.auth_contents = build_auth_json(&request.api_key);\n    Ok(request)\n}\n\nfn relay_protocol(value: &str) -> RelayProtocol {\n    match value.trim().to_ascii_lowercase().as_str() {\n        \"chat\" | \"chat_completions\" | \"chat-completions\" => RelayProtocol::ChatCompletions,\n        _ => RelayProtocol::Responses,\n    }","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/provider_import.rs#L210-L246","documentation":"Thrown by `normalize_request` in provider_import.rs when the provider's `base_url` is empty or whitespace-only after trimming (the code also strips trailing slashes but the emptiness check comes first). The base URL is mandatory because it is baked into the generated config.toml endpoint for the provider. Without it the generated provider config would point nowhere, so import aborts.","triggerScenarios":"Calling `import_provider_with_store` with a request whose `base_url` is `\"\"`, `\" \"`, or missing, after the name check passed; normalize_request bails at provider_import.rs:228.","commonSituations":"User pasted only an API key and forgot the endpoint; a clipboard/template import left the URL field blank; building the request programmatically with a default-constructed struct.","solutions":["Provide the relay/provider endpoint, e.g. `base_url: \"https://api.example.com/v1\"`, in the request.","Validate the URL is non-empty and well-formed in the UI/CLI before invoking the import.","Check that any template or JSON source actually populates `base_url` (no placeholder left empty)."],"exampleFix":"// before\nrequest.base_url = \"\".to_string();\n// after\nrequest.base_url = \"https://api.example.com/v1\".to_string();","handlingStrategy":"validation","validationCode":"let base = request.base_url.trim().trim_end_matches('/');\nif base.is_empty() || !base.starts_with(\"http\") {\n    return Err(anyhow::anyhow!(\"Base URL 不能为空且必须是合法 http(s) URL\"));\n}\nrequest.base_url = base.to_string();","typeGuard":"fn has_base_url(req: &ProviderImportRequest) -> bool {\n    req.base_url.trim().starts_with(\"http://\") || req.base_url.trim().starts_with(\"https://\")\n}","tryCatchPattern":"match import_provider_with_store(&store, request) {\n    Ok(p) => Ok(p),\n    Err(e) if e.to_string().contains(\"Base URL 为空\") => Err(anyhow::anyhow!(\"请在表单中填写中转站地址\")),\n    Err(e) => Err(e),\n}","preventionTips":["Use a URL input with client-side format validation in the UI.","Trim and strip trailing slashes from user-entered URLs before submitting.","Check templates/JSON imports actually populate base_url."],"tags":["validation","provider-import","empty-field","url"],"backgroundTag":"empty-required-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"}