googleworkspace/cli · error · GwsError

Attachment response missing 'data' field for {attachment_id}

Error message

Attachment response missing 'data' field for {attachment_id}

What it means

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.

Source

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

        let status = resp.status().as_u16();
        let err = resp
            .text()
            .await
            .unwrap_or_else(|_| "(error body unreadable)".to_string());
        return Err(build_api_error(
            status,
            &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,

View on GitHub (pinned to a3768d0e82)

Solutions

  1. Re-fetch the parent message and confirm the part still lists an attachmentId.
  2. Retry the download once — transient inconsistencies resolve.
  3. If using a mock server, return `{"data": "<base64url>", "size": N}`.
  4. Update the CLI if Gmail changed the envelope shape; then report upstream.
Defensive patterns

Strategy: type-guard

Validate before calling

// Guard before decode: confirm the envelope carries a data string
let data = body.get("data").and_then(|v| v.as_str());
if data.is_none() {
    eprintln!("attachment {attachment_id} returned no payload; re-fetching message");
    refresh_message_metadata().await?;
}

Type guard

fn attachment_has_data(v: &serde_json::Value) -> bool {
    v.get("data").and_then(|d| d.as_str()).is_some_and(|s| !s.is_empty())
}

Try / catch

// In multi-part downloads, skip or re-resolve parts whose envelope lacks data instead of aborting all parts:
if !attachment_has_data(&body) {
    log_missing_part(attachment_id);
    continue; // or re-fetch message metadata to get a fresh attachmentId
}

Prevention

When it happens

Trigger: 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.

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

Related errors


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