googleworkspace/cli · error · GwsError

Failed to decode attachment data: {e}

Error message

Failed to decode attachment data: {e}

What it means

`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.

Source

Thrown at crates/google-workspace-cli/src/helpers/gmail/mod.rs:730

            &err,
            &format!("Failed to fetch attachment {attachment_id} from message {message_id}"),
        ));
    }

    let body: Value = resp
        .json()
        .await
        .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to parse attachment JSON: {e}")))?;

    let data_str = body.get("data").and_then(|v| v.as_str()).ok_or_else(|| {
        GwsError::Other(anyhow::anyhow!(
            "Attachment response missing 'data' field for {attachment_id}"
        ))
    })?;

    URL_SAFE
        .decode(data_str)
        .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to decode attachment data: {e}")))
}

/// Fetch binary data for selected original parts, converting them to `Attachment`s.
///
/// Performs a size preflight check using metadata before downloading, then fetches
/// parts sequentially. `existing_bytes` is the cumulative size of user-supplied
/// `--attach` files, counted against the combined size limit.
pub(super) async fn fetch_original_parts(
    client: &reqwest::Client,
    token: &str,
    message_id: &str,
    parts: &[OriginalPart],
    existing_bytes: u64,
) -> Result<Vec<Attachment>, GwsError> {
    // Size preflight: check metadata sizes before downloading anything
    let total_metadata_size: u64 = parts.iter().map(|p| p.size).sum();
    if existing_bytes + total_metadata_size > MAX_TOTAL_ATTACHMENT_BYTES {
        return Err(GwsError::Validation(format!(

View on GitHub (pinned to a3768d0e82)

Solutions

  1. 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).
  2. Print the tail of `data_str` to check for missing '=' or '+/' characters.
  3. For stub servers, generate fixtures with the URL-safe alphabet.
  4. Update the CLI — engine selection lives in helpers/gmail/mod.rs and is patchable.

Example fix

// before: strict padded URL-safe decode fails on Gmail's unpadded base64url
URL_SAFE.decode(data_str).map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to decode attachment data: {e}")))

// after: accept both padded and unpadded base64url
fn decode_b64url_flexible(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
    use base64::Engine;
    let trimmed = s.trim_end_matches('=');
    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(trimmed)
}
decode_b64url_flexible(data_str).map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to decode attachment data: {e}")))
Defensive patterns

Strategy: validation

Validate before calling

// Normalize before decode: trim whitespace, re-pad if needed, reject other-alphabet chars
fn normalized_b64url(s: &str) -> Result<String, base64::DecodeError> {
    let t = s.trim();
    if t.chars().any(|c| c == '+' || c == '/' || c.is_whitespace()) {
        return Err(base64::DecodeError::InvalidByte(0, b' '));
    }
    let pad = (4 - t.len() % 4) % 4;
    Ok(format!("{t}{}", "=".repeat(pad)))
}

Type guard

fn is_padded_b64url(s: &str) -> bool {
    !s.is_empty() && s.len() % 4 == 0 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'=')
}

Try / catch

// Decode leniently: try padded, fall back to NO_PAD after stripping '='
use base64::Engine;
let bytes = URL_SAFE
    .decode(data_str)
    .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(data_str.trim_end_matches('=')))
    .map_err(|e| anyhow::anyhow!("attachment base64 undecodable: {e}"))?;

Prevention

When it happens

Trigger: 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 ('-','_').

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16). Data as JSON: /api/errors/f0b7c3288cb52683. Report an issue: GitHub.