{"record":{"id":"03afa8bb174e8990","repo":"BigPizzaV3/CodexPlusPlus","slug":"error-03afa8","errorCode":null,"errorMessage":"倍率接口返回了无效用户倍率","messagePattern":"倍率接口返回了无效用户倍率","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/sub2api.rs","lineNumber":121,"sourceCode":"fn validate_sub2api_billing_response(response: &Sub2ApiBillingResponse) -> anyhow::Result<()> {\n    if response.object != \"sub2api.key_billing\"\n        || response.schema_version != 1\n        || response.billing_scope != \"token\"\n    {\n        anyhow::bail!(\"倍率接口返回结构不是 sub2api.key_billing\");\n    }\n    for value in [\n        response.group_rate_multiplier,\n        response.resolved_rate_multiplier,\n        response.effective_rate_multiplier,\n    ] {\n        if !value.is_finite() || value < 0.0 {\n            anyhow::bail!(\"倍率接口返回了无效倍率\");\n        }\n    }\n    if let Some(value) = response.user_rate_multiplier {\n        if !value.is_finite() || value < 0.0 {\n            anyhow::bail!(\"倍率接口返回了无效用户倍率\");\n        }\n    }\n    if response.observed_at.trim().is_empty() {\n        anyhow::bail!(\"倍率接口缺少观测时间\");\n    }\n    Ok(())\n}\n\nfn sub2api_error_message(body: &str) -> Option<String> {\n    let payload: Value = serde_json::from_str(body).ok()?;\n    for path in [&[\"error\", \"message\"][..], &[\"message\"][..], &[\"error\"][..]] {\n        let mut current = &payload;\n        for key in path {\n            current = current.get(*key)?;\n        }\n        if let Some(message) = current\n            .as_str()\n            .map(str::trim)","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/sub2api.rs#L103-L139","documentation":"Thrown by validate_sub2api_billing_response inside fetch_sub2api_billing_info (crates/codex-plus-core/src/sub2api.rs). After GET {base}/v1/sub2api/billing returns HTTP 200 with parseable JSON, every multiplier is sanity-checked with !value.is_finite() || value < 0.0. This variant fires only for the optional user_rate_multiplier field (Option<f64> with #[serde(default)]): the field was present in the response as a number, but that number is NaN, infinite, or negative. The three mandatory multipliers (group/resolved/effective) passed the identical check just before it.","triggerScenarios":"Calling fetch_sub2api_billing_info(&profile) where the upstream JSON contains \"user_rate_multiplier\": -0.5 (negative) or \"user_rate_multiplier\": 1e999 (serde_json parses this to f64 infinity). A JSON null or an absent field does NOT trigger it — serde maps those to None and the branch is skipped.","commonSituations":"A sub2api server with a per-user discount override misconfigured to a negative value; schema drift after an upstream version change; a relay/gateway in front of the endpoint corrupting numeric fields; mock servers in tests returning placeholder values like -1.","solutions":["Replay the request with the same credentials (curl -H \"Authorization: Bearer <key>\" <base>/v1/sub2api/billing) and inspect the actual user_rate_multiplier value","Fix the value on the sub2api server so it is a finite number >= 0, or remove the per-user override so the field is absent/null","If you own the calling code, degrade instead of aborting: fall back to resolved_rate_multiplier or 1.0 and surface a warning","Retry after the upstream fix — validation runs on every fetch, so no client-side cache needs clearing"],"exampleFix":"// before\nlet billing = fetch_sub2api_billing_info(&profile).await?;\n\n// after\nlet billing = match fetch_sub2api_billing_info(&profile).await {\n    Ok(info) => info,\n    Err(err) if err.to_string().contains(\"无效用户倍率\") => {\n        tracing::warn!(?err, \"invalid user multiplier, using defaults\");\n        default_billing_info(&profile)\n    }\n    Err(err) => return Err(err),\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match fetch_sub2api_billing_info(&profile).await {\n    Ok(info) => { /* use info */ }\n    Err(err) if err.to_string().contains(\"无效用户倍率\") => {\n        // fall back to cached/default multipliers and warn\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Cache the last valid Sub2ApiBillingInfo so a bad upstream response degrades instead of breaking the UI","If you operate the sub2api server, validate multipliers (finite, >= 0) at write time","Pin schema_version 1 in contract tests so upstream schema drift surfaces in CI, not as runtime NaN/infinity"],"tags":["sub2api","billing","rate-multiplier","response-validation","float"],"backgroundTag":"invalid-api-response-field","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}