{"record":{"id":"0e87473d4e2d0f7e","repo":"xai-org/grok-build","slug":"failed-to-parse-billing-data-e","errorCode":null,"errorMessage":"Failed to parse billing data: {e}","messagePattern":"Failed to parse billing data: (.+?)","errorType":"exception","errorClass":"acp::Error","httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-shell/src/extensions/billing.rs","lineNumber":270,"sourceCode":"            Some(serde_json::json!({\n                \"status\": status,\n                \"detail\": detail,\n            })),\n        );\n\n        return Err(acp::Error::internal_error().data(format!(\"Billing service error: {detail}\")));\n    }\n\n    let mut billing: BillingConfigResponse = credits_resp.json().await.map_err(|e| {\n        tracing::error!(error = %e, \"billing: failed to parse response\");\n        xai_grok_telemetry::unified_log::warn(\n            \"billing: failed to parse response\",\n            None,\n            Some(serde_json::json!({ \"error\": e.to_string() })),\n        );\n        acp::Error::internal_error().data(format!(\"Failed to parse billing data: {e}\"))\n    })?;\n\n    // Enrich with fields from remote settings.\n    let rs = agent.cfg.borrow().remote_settings.clone();\n    billing.on_demand_enabled = rs.as_ref().and_then(|rs| rs.on_demand_enabled);\n    billing.subscription_tier = rs.as_ref().and_then(|rs| {\n        rs.subscription_tier_display\n            .clone()\n            .or_else(|| rs.subscription_tier.clone())\n    });\n\n    // Every prompt / /usage / poll path hits `x.ai/billing`; log the fetched\n    // credits snapshot so support can correlate limit UX with real balances.\n    xai_grok_telemetry::unified_log::info(\n        \"billing: fetched credits config\",\n        None,\n        Some(billing_unified_log_ctx(&billing)),\n    );\n\n    to_raw_response(&billing)","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-shell/src/extensions/billing.rs#L252-L288","documentation":"The billing endpoint returned HTTP 200, but the body could not be deserialized into `BillingConfigResponse`. The handler logs the parse failure via unified telemetry and returns an ACP internal error 'Failed to parse billing data: {e}'. This is a contract mismatch between what the upstream service sent and the schema the shell expects.","triggerScenarios":"`credits_resp.json::<BillingConfigResponse>()` returns `Err(e)` — response body is HTML/JSON from a proxy error page, fields have unexpected types (string vs number), required fields are missing after an API change, or an interceptor returned an empty body.","commonSituations":"API version drift between shell and billing service (schema change); captive portal or proxy injecting HTML; partial/empty 200 responses during upstream incidents; response envelope renamed (e.g. `on_demand_enabled` type changed).","solutions":["Read `{e}` (also in telemetry data) to identify which field failed to deserialize.","Capture the raw response body (curl the credits endpoint) and compare it against the `BillingConfigResponse` schema.","Upgrade or downgrade the shell so its `BillingConfigResponse` matches the deployed billing API version.","Check for proxies/captive portals returning non-JSON bodies with 200 status."],"exampleFix":"// before: API changed field type, shell expects bool\n{ \"on_demand_enabled\": \"true\" }\n// after: upstream must send the expected type\n{ \"on_demand_enabled\": true }","handlingStrategy":"type-guard","validationCode":"// probe the endpoint and check the body is JSON before relying on the parsed result\nconst body = await fetch(creditsUrl, { headers: auth(token) }).then(r => r.text());\nconst isJson = body.trimStart().startsWith(\"{\") || body.trimStart().startsWith(\"[\");\nif (!isJson) throw new Error(\"billing endpoint returned non-JSON body\");","typeGuard":"fn is_billing_config(v: &serde_json::Value) -> bool {\n    serde_json::from_value::<BillingConfigResponse>(v.clone()).is_ok()\n}","tryCatchPattern":"try {\n  const billing = await acp.request(\"billing_get\");\n} catch (e) {\n  if (String(e.message).startsWith(\"Failed to parse billing data\")) {\n    dumpRawResponseForDiagnostics(); // compare body to BillingConfigResponse schema\n  } else { throw e; }\n}","preventionTips":["Keep shell and billing API versions aligned; generate types from the API schema","Guard against proxies returning HTML error pages with 200 status","Snapshot-test BillingConfigResponse against live API responses","Log the raw body when deserialization fails"],"tags":["deserialization","serde","billing","schema-mismatch"],"backgroundTag":"response-deserialization-failed","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}