{"record":{"id":"0798c387965f1d20","repo":"BigPizzaV3/CodexPlusPlus","slug":"api-key","errorCode":null,"errorMessage":"API Key 为空","messagePattern":"API Key 为空","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/provider_import.rs","lineNumber":231,"sourceCode":"        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    }\n}\n\nfn relay_mode(value: &str) -> RelayMode {","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/provider_import.rs#L213-L249","documentation":"Thrown by `normalize_request` in provider_import.rs when `api_key` is empty after trimming. The key is required because it is embedded into the generated auth/config contents for the provider; an import without a key would produce a config that cannot authenticate. This is the third of three sequential required-field checks (name, base_url, api_key).","triggerScenarios":"Calling `import_provider_with_store` with an `api_key` of `\"\"` or whitespace-only while name and base_url are valid; bails at provider_import.rs:231.","commonSituations":"User skipped the key field intending to fill it later; environment variable holding the key was unset so an empty string was interpolated; key was stripped by a sanitizer or secret-scanning step.","solutions":["Supply the actual API key in `request.api_key` before importing.","If the key comes from an env var, check it is set and non-empty (e.g. `std::env::var(\"MY_KEY\")?`) before building the request.","In UI flows, require the key field (password input) before enabling the import button."],"exampleFix":"// before\nlet req = ProviderImportRequest { api_key: std::env::var(\"RELAY_KEY\").unwrap_or_default(), .. };\n// after\nlet req = ProviderImportRequest { api_key: std::env::var(\"RELAY_KEY\").expect(\"RELAY_KEY must be set\"), .. };","handlingStrategy":"validation","validationCode":"let key = request.api_key.trim();\nif key.is_empty() {\n    return Err(anyhow::anyhow!(\"API Key 不能为空\"));\n}\nrequest.api_key = key.to_string();","typeGuard":"fn has_api_key(req: &ProviderImportRequest) -> bool { !req.api_key.trim().is_empty() }","tryCatchPattern":"match import_provider_with_store(&store, request) {\n    Ok(p) => Ok(p),\n    Err(e) if e.to_string().contains(\"API Key 为空\") => Err(anyhow::anyhow!(\"请填写 API Key（可从环境变量读取）\")),\n    Err(e) => Err(e),\n}","preventionTips":["Read keys from env/secret stores with explicit error on absence, never unwrap_or_default(\"\").","Require the key field (password input) before enabling import in the UI.","Never log or persist the key in plaintext outside the designated store."],"tags":["validation","provider-import","api-key","empty-field"],"backgroundTag":"missing-api-key","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"}