{"record":{"id":"f0b7c3288cb52683","repo":"googleworkspace/cli","slug":"failed-to-decode-attachment-data-e","errorCode":null,"errorMessage":"Failed to decode attachment data: {e}","messagePattern":"Failed to decode attachment data: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/mod.rs","lineNumber":730,"sourceCode":"            &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,\n    parts: &[OriginalPart],\n    existing_bytes: u64,\n) -> Result<Vec<Attachment>, GwsError> {\n    // Size preflight: check metadata sizes before downloading anything\n    let total_metadata_size: u64 = parts.iter().map(|p| p.size).sum();\n    if existing_bytes + total_metadata_size > MAX_TOTAL_ATTACHMENT_BYTES {\n        return Err(GwsError::Validation(format!(","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/mod.rs#L712-L748","documentation":"`URL_SAFE.decode(data_str)` failed: the `data` field of the attachment response was not decodable as padded, URL-safe base64. Gmail historically returns base64url that may omit padding characters ('='), while the strict `URL_SAFE` engine in base64 0.22 requires canonical padding — unpadded input is the classic trigger. Corrupted or whitespace-laden payloads also fail here.","triggerScenarios":"Gmail returning unpadded base64url (e.g. data length not a multiple of 4); JSON unescaping artifacts introducing stray characters; an interceptor corrupting the payload; using a stub that emits standard base64 ('+','/') instead of URL-safe ('-','_').","commonSituations":"Intermittent failures across many attachments — only those whose encoded length happens to need padding fail, making it look random; test fixtures generated with the wrong alphabet or without padding.","solutions":["If it fails only for some attachments, suspect padding: this is fixed by decoding with `base64::engine::general_purpose::URL_SAFE_NO_PAD` or by appending the needed '=' padding first (see example fix).","Print the tail of `data_str` to check for missing '=' or '+/' characters.","For stub servers, generate fixtures with the URL-safe alphabet.","Update the CLI — engine selection lives in helpers/gmail/mod.rs and is patchable."],"exampleFix":"// before: strict padded URL-safe decode fails on Gmail's unpadded base64url\nURL_SAFE.decode(data_str).map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to decode attachment data: {e}\")))\n\n// after: accept both padded and unpadded base64url\nfn decode_b64url_flexible(s: &str) -> Result<Vec<u8>, base64::DecodeError> {\n    use base64::Engine;\n    let trimmed = s.trim_end_matches('=');\n    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(trimmed)\n}\ndecode_b64url_flexible(data_str).map_err(|e| GwsError::Other(anyhow::anyhow!(\"Failed to decode attachment data: {e}\")))","handlingStrategy":"validation","validationCode":"// Normalize before decode: trim whitespace, re-pad if needed, reject other-alphabet chars\nfn normalized_b64url(s: &str) -> Result<String, base64::DecodeError> {\n    let t = s.trim();\n    if t.chars().any(|c| c == '+' || c == '/' || c.is_whitespace()) {\n        return Err(base64::DecodeError::InvalidByte(0, b' '));\n    }\n    let pad = (4 - t.len() % 4) % 4;\n    Ok(format!(\"{t}{}\", \"=\".repeat(pad)))\n}","typeGuard":"fn is_padded_b64url(s: &str) -> bool {\n    !s.is_empty() && s.len() % 4 == 0 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'=')\n}","tryCatchPattern":"// Decode leniently: try padded, fall back to NO_PAD after stripping '='\nuse base64::Engine;\nlet bytes = URL_SAFE\n    .decode(data_str)\n    .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(data_str.trim_end_matches('=')))\n    .map_err(|e| anyhow::anyhow!(\"attachment base64 undecodable: {e}\"))?;","preventionTips":["Never assume Google base64url is padded — always decode with URL_SAFE_NO_PAD on the trimmed input or re-pad yourself.","Generate test fixtures with the URL-safe alphabet (-, _), not standard base64 (+, /).","If only some attachments fail, suspect a length-dependent padding issue, not corruption."],"tags":["gmail","attachment","base64","encoding","padding"],"backgroundTag":"base64-decode-failed","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}