{"record":{"id":"a7c28b13cb529760","repo":"googleworkspace/cli","slug":"failed-to-parse-message-e","errorCode":null,"errorMessage":"Failed to parse message: {e}","messagePattern":"Failed to parse message: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/mod.rs","lineNumber":402,"sourceCode":"    .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to fetch message: {e}\")))?;\n\n    if !resp.status().is_success() {\n        let status = resp.status().as_u16();\n        let body = resp\n            .text()\n            .await\n            .unwrap_or_else(|_| \"(error body unreadable)\".to_string());\n        return Err(build_api_error(\n            status,\n            &body,\n            &format!(\"Failed to fetch message {message_id}\"),\n        ));\n    }\n\n    let msg: Value = resp\n        .json()\n        .await\n        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to parse message: {e}\")))?;\n\n    parse_original_message(&msg)\n}\n\n/// Build a `GwsError::Api` from an HTTP error response body, parsing the\n/// Google JSON error format when possible. Modeled after the executor's\n/// `handle_error_response`, extracting message, reason, and enable URL.\npub(super) fn build_api_error(status: u16, body: &str, context: &str) -> GwsError {\n    let err_json: Option<Value> = serde_json::from_str(body).ok();\n    let err_obj = err_json.as_ref().and_then(|v| v.get(\"error\"));\n    let message = err_obj\n        .and_then(|e| e.get(\"message\"))\n        .and_then(|m| m.as_str())\n        .unwrap_or(body)\n        .to_string();\n    let reason = err_obj\n        .and_then(|e| e.get(\"errors\"))\n        .and_then(|e| e.as_array())","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/mod.rs#L384-L420","documentation":"After a successful HTTP 2xx from `GET /gmail/v1/users/me/messages/{id}?format=full`, `resp.json::<Value>()` failed — the body was not valid JSON. This means Gmail (or an intercepting proxy) returned a 200 with a non-JSON payload, e.g. an HTML error page, an empty body, or a truncated response.","triggerScenarios":"Captive portal or proxy returning 200 with an HTML login page; response truncated by a flaky connection so the body is cut mid-JSON; a gateway rewriting responses; extremely rare API glitch serving an error page with 200.","commonSituations":"Corporate TLS-inspection appliances, hotel wifi, or a mid-response connection reset that reqwest still surfaces as a complete body read; scripts parsing via the CLI in sandboxed CI with weird egress.","solutions":["Retry the command — transient truncation resolves itself.","Check whether a proxy/captive portal intercepts googleapis.com and bypass it.","Reproduce the raw body: `curl -H \"Authorization: Bearer $TOKEN\" 'https://gmail.googleapis.com/gmail/v1/users/me/messages/<ID>?format=full' | head -c 200` to see if it is HTML.","Verify the OAuth token is a real Gmail token, not one from a mock/stub server."],"exampleFix":"// before: json() alone loses the body that failed to parse\nlet msg: Value = resp.json().await.map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to parse message: {e}\")))?;\n\n// after: capture the raw body to surface what was actually returned\nlet text = resp.text().await.map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to read message body: {e}\")))?;\nlet msg: Value = serde_json::from_str(&text).map_err(|e| {\n    GwsError::Other(anyhow::anyhow!(\"Failed to parse message: {e} (body starts: {})\", &text[..text.len().min(120)]))\n})?;","handlingStrategy":"validation","validationCode":"// Assert JSON content-type before parsing, catching interceptor HTML early\nlet ct = resp.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or(\"\");\nif !ct.starts_with(\"application/json\") {\n    return Err(anyhow::anyhow!(\"expected JSON from Gmail, got '{ct}' — proxy interception?\"));\n}","typeGuard":"fn looks_like_gmail_message(v: &serde_json::Value) -> bool {\n    v.get(\"id\").and_then(|x| x.as_str()).is_some() && v.get(\"payload\").map(|p| p.is_object()).unwrap_or(false)\n}","tryCatchPattern":"// Parse via text() first so failures can include a body snippet:\nlet text = resp.text().await?;\nmatch serde_json::from_str::<Value>(&text) {\n    Ok(v) if looks_like_gmail_message(&v) => v,\n    Ok(_) => return Err(anyhow::anyhow!(\"200 response missing id/payload fields\")),\n    Err(e) => return Err(anyhow::anyhow!(\"invalid JSON from Gmail: {e}; body[0..120]={}\", &text[..text.len().min(120)])),\n}","preventionTips":["Check Content-Type before json() on every Google API call you make yourself.","Route around TLS-inspecting proxies for googleapis.com in scripts that parse responses.","Retry once on parse failure — truncation is often transient."],"tags":["gmail","json","response-parsing","proxy","captcha-portal"],"backgroundTag":"invalid-json-response","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}