{"record":{"id":"0949b4bb1eab65fc","repo":"googleworkspace/cli","slug":"failed-to-parse-calendar-list-e","errorCode":null,"errorMessage":"Failed to parse calendar list: {e}","messagePattern":"Failed to parse calendar list: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/calendar.rs","lineNumber":283,"sourceCode":"        .bearer_auth(&token)\n        .send()\n        .await\n        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to list calendars: {e}\")))?;\n\n    if !list_resp.status().is_success() {\n        let err = list_resp.text().await.unwrap_or_default();\n        return Err(GwsError::Api {\n            code: 0,\n            message: err,\n            reason: \"calendarList_failed\".to_string(),\n            enable_url: None,\n        });\n    }\n\n    let list_json: Value = list_resp\n        .json()\n        .await\n        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to parse calendar list: {e}\")))?;\n\n    let calendars = list_json\n        .get(\"items\")\n        .and_then(|i| i.as_array())\n        .cloned()\n        .unwrap_or_default();\n\n    // 2. For each calendar, fetch events concurrently\n    use futures_util::stream::{self, StreamExt};\n\n    // Pre-filter calendars and collect owned data to avoid lifetime issues\n    struct CalInfo {\n        id: String,\n        summary: String,\n    }\n    let filtered_calendars: Vec<CalInfo> = calendars\n        .iter()\n        .filter_map(|cal| {","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/calendar.rs#L265-L301","documentation":"The calendarList request returned a success status, but response.json() could not deserialize the body as JSON. So the transport worked, the server said OK, yet the payload is not the expected JSON document — usually an HTML page or an empty/garbled body from an intermediary, since the Calendar API itself reliably returns JSON on 2xx.","triggerScenarios":"Captive portal / proxy returning an HTML terms page with 200; a transparent proxy appending junk; empty 200 body from a misrouting gateway; response gzip/charset mangling; API edge returning an empty body on partial outage.","commonSituations":"Corporate web filters intercepting Google domains; hotel/airport Wi-Fi captive portals; flaky home routers with 'web acceleration'; rare Google frontend incidents.","solutions":["Reproduce the raw body: curl -s 'https://www.googleapis.com/calendar/v3/users/me/calendarList' -H \"Authorization: Bearer $TOKEN\" | head -c 400 — if it is HTML, fix the proxy/network","Bypass or correctly configure the intercepting proxy / captive portal","If the body looks like valid JSON, report a bug with the body snippet"],"exampleFix":"// before: blind json() losing the payload on failure\nlet list_json: Value = list_resp.json().await.map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to parse calendar list: {e}\")))?;\n\n// after: capture the body for diagnosis\nlet body = list_resp.text().await.unwrap_or_default();\nlet list_json: Value = serde_json::from_str(&body).map_err(|e| {\n    GwsError::Other(anyhow::anyhow!(\"calendarList body was not JSON ({e}): {}\", &body[..body.len().min(200)]))\n})?;","handlingStrategy":"try-catch","validationCode":"// Fetch text first, validate JSON shape before trusting it\nlet body = resp.text().await?;\nlet v: serde_json::Value = serde_json::from_str(&body)?;\nanyhow::ensure!(v.get(\"items\").map(|i| i.is_array()).unwrap_or(false) || v.get(\"kind\").is_some(),\n    \"unexpected calendarList payload (not a Calendar API document)\");","typeGuard":"fn is_calendar_list_doc(v: &serde_json::Value) -> bool {\n    v.is_object() && (v.get(\"items\").is_some_and(|i| i.is_array()) || v.get(\"kind\").is_some())\n}","tryCatchPattern":"let body = resp.text().await.unwrap_or_default();\nlet parsed: serde_json::Value = match serde_json::from_str(&body) {\n    Ok(v) => v,\n    Err(e) => {\n        // non-JSON 2xx body: proxy/captive portal — log first 200 bytes and surface a network-config error\n        anyhow::bail!(\"calendarList returned non-JSON body ({e}): {}\", &body.chars().take(200).collect::<String>())\n    }\n};","preventionTips":["Never assume 2xx implies JSON on networks you do not control — parse defensively","Check Content-Type and body prefix when responses come through proxies","Log a short body excerpt on parse failures; it turns a mystery into an obvious captive-portal/proxy page"],"tags":["calendar","json","parsing","proxy"],"backgroundTag":"invalid-json-response","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}