{"record":{"id":"8270255cb545e8ae","repo":"BigPizzaV3/CodexPlusPlus","slug":"error-827025","errorCode":null,"errorMessage":"倍率接口缺少观测时间","messagePattern":"倍率接口缺少观测时间","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/sub2api.rs","lineNumber":125,"sourceCode":"    {\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)\n            .filter(|value| !value.is_empty())\n        {\n            return Some(message.to_string());\n        }","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/sub2api.rs#L107-L143","documentation":"Thrown by validate_sub2api_billing_response during fetch_sub2api_billing_info when the parsed billing response has an observed_at field that is empty or only whitespace. observed_at is a required String in Sub2ApiBillingResponse, so deserialization itself succeeds only when the key exists — but its content is checked for non-emptiness afterwards. This is the final validation step before the response is accepted as Sub2ApiBillingInfo.","triggerScenarios":"GET {base}/v1/sub2api/billing returns 200 JSON with \"observed_at\": \"\" or \"observed_at\": \"   \". All object/schema_version and multiplier checks passed; only the timestamp string is blank.","commonSituations":"An upstream sub2api deployment that stamps the timestamp in a different layer and omits it on some code paths; a stub server in tests that fills every field except observed_at; an older server version whose schema predates observed_at but still passes the object/schema_version check.","solutions":["Check the raw response body for the observed_at field using the same bearer key","Fix the sub2api server to always emit a non-empty ISO-8601 observation timestamp","If you control the caller, fall back to the local clock with a warning instead of failing the whole billing view","Retry after the upstream fix — the field is re-validated on every fetch"],"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        let mut info = last_known_billing(&profile);\n        info.observed_at = chrono::Utc::now().to_rfc3339(); // local fallback\n        info\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        // rebuild from last-known values, stamp observed_at with local time, warn\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Contract-test that observed_at is a non-empty ISO-8601 string","Probe the billing endpoint in monitoring so missing timestamps are caught before users see failures","Treat the timestamp as display metadata: fall back to local time rather than blocking the billing view"],"tags":["sub2api","billing","timestamp","response-validation"],"backgroundTag":"missing-required-json-field","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}