{"record":{"id":"31085b574ebc0cdb","repo":"googleworkspace/cli","slug":"attachment-response-missing-data-field-for-atta","errorCode":null,"errorMessage":"Attachment response missing 'data' field for {attachment_id}","messagePattern":"Attachment response missing 'data' field for (.+?)","errorType":"validation","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/mod.rs","lineNumber":723,"sourceCode":"        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(\n    client: &reqwest::Client,\n    token: &str,\n    message_id: &str,","sourceCodeStart":705,"sourceCodeEnd":741,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/mod.rs#L705-L741","documentation":"The attachment JSON parsed successfully but had no `data` string field — `body.get(\"data\").and_then(|v| v.as_str())` returned None. Per the Gmail API contract the response must contain `data` (base64url payload); its absence means the server returned an object of an unexpected shape, e.g. an error envelope with 200, a `size`-only object for an attachment that no longer exists, or an API version change.","triggerScenarios":"Attachment deleted or expired on the server between message fetch and attachment download; Gmail returning an error object with HTTP 200; a stub/test server returning `{\"size\": 0}`; schema drift after an API update.","commonSituations":"Forwarding a message whose attachment was stripped by retention policy or the user; racing another client that deleted the attachment; integration tests with incomplete fixtures.","solutions":["Re-fetch the parent message and confirm the part still lists an attachmentId.","Retry the download once — transient inconsistencies resolve.","If using a mock server, return `{\"data\": \"<base64url>\", \"size\": N}`.","Update the CLI if Gmail changed the envelope shape; then report upstream."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Guard before decode: confirm the envelope carries a data string\nlet data = body.get(\"data\").and_then(|v| v.as_str());\nif data.is_none() {\n    eprintln!(\"attachment {attachment_id} returned no payload; re-fetching message\");\n    refresh_message_metadata().await?;\n}","typeGuard":"fn attachment_has_data(v: &serde_json::Value) -> bool {\n    v.get(\"data\").and_then(|d| d.as_str()).is_some_and(|s| !s.is_empty())\n}","tryCatchPattern":"// In multi-part downloads, skip or re-resolve parts whose envelope lacks data instead of aborting all parts:\nif !attachment_has_data(&body) {\n    log_missing_part(attachment_id);\n    continue; // or re-fetch message metadata to get a fresh attachmentId\n}","preventionTips":["Re-fetch the parent message if any attachment envelope lacks data — attachmentIds go stale after server-side deletion.","Type-guard the envelope before decoding; treat missing data as per-part skip in batch flows.","Keep mock servers returning the full `{data, size}` envelope."],"tags":["gmail","attachment","schema","missing-field","api-contract"],"backgroundTag":"missing-response-field","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}