{"record":{"id":"1bf78ff49d710493","repo":"googleworkspace/cli","slug":"failed-to-parse-attachment-json-e","errorCode":null,"errorMessage":"Failed to parse attachment JSON: {e}","messagePattern":"Failed to parse attachment JSON: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/mod.rs","lineNumber":720,"sourceCode":"        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to fetch attachment: {e}\")))?;\n\n    if !resp.status().is_success() {\n        let status = resp.status().as_u16();\n        let err = resp\n            .text()\n            .await\n            .unwrap_or_else(|_| \"(error body unreadable)\".to_string());\n        return Err(build_api_error(\n            status,\n            &err,\n            &format!(\"Failed to fetch attachment {attachment_id} from message {message_id}\"),\n        ));\n    }\n\n    let body: Value = resp\n        .json()\n        .await\n        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to parse attachment JSON: {e}\")))?;\n\n    let data_str = body.get(\"data\").and_then(|v| v.as_str()).ok_or_else(|| {\n        GwsError::Other(anyhow::anyhow!(\n            \"Attachment response missing 'data' field for {attachment_id}\"\n        ))\n    })?;\n\n    URL_SAFE\n        .decode(data_str)\n        .map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to decode attachment data: {e}\")))\n}\n\n/// Fetch binary data for selected original parts, converting them to `Attachment`s.\n///\n/// Performs a size preflight check using metadata before downloading, then fetches\n/// parts sequentially. `existing_bytes` is the cumulative size of user-supplied\n/// `--attach` files, counted against the combined size limit.\npub(super) async fn fetch_original_parts(","sourceCodeStart":702,"sourceCodeEnd":738,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/mod.rs#L702-L738","documentation":"The attachments endpoint returned 2xx but the body failed `resp.json::<Value>()`. Gmail's attachment endpoint always returns JSON (`{ \"data\": \"<base64url>\", \"size\": N }`), so non-JSON here means an interceptor rewrote the response or the body was truncated mid-download.","triggerScenarios":"Proxy replacing the attachment response with HTML; connection cut during a multi-megabyte base64 body so all retries truncate; auth gateway injecting a challenge page with 200.","commonSituations":"Large-attachment forwards through TLS-inspecting proxies; captive portals mid-session.","solutions":["Retry once; transient truncation is the most common cause.","Test the raw endpoint with curl and a bearer token to inspect content type and first bytes.","Bypass the proxy for gmail.googleapis.com.","If persistent, capture the body prefix for diagnosis."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"let ct = resp.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or(\"\");\nif !ct.contains(\"json\") {\n    return Err(anyhow::anyhow!(\"attachment endpoint returned '{ct}' — interception suspected\"));\n}","typeGuard":"fn is_attachment_envelope(v: &serde_json::Value) -> bool {\n    v.get(\"data\").map(|d| d.is_string()).unwrap_or(false)\n        || v.get(\"size\").and_then(|s| s.as_u64()).is_some()\n}","tryCatchPattern":"let text = resp.text().await?;\nlet body: Value = serde_json::from_str(&text)\n    .map_err(|e| anyhow::anyhow!(\"attachment body not JSON ({e}): {}\", &text[..text.len().min(120)]))?;\nif !is_attachment_envelope(&body) { return Err(anyhow::anyhow!(\"unexpected attachment envelope\")); }","preventionTips":["Content-type check before json() catches interceptor HTML on large attachment responses.","Retry once — mid-body truncation on big base64 payloads is common on bad links.","Keep test fixtures shaped `{\"data\": \"...\", \"size\": N}`."],"tags":["gmail","attachment","json","response-parsing","proxy"],"backgroundTag":"invalid-json-response","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}