googleworkspace/cli · error · GwsError

Failed to parse attachment JSON: {e}

Error message

Failed to parse attachment JSON: {e}

What it means

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.

Source

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

        .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch attachment: {e}")))?;

    if !resp.status().is_success() {
        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(

View on GitHub (pinned to a3768d0e82)

Solutions

  1. Retry once; transient truncation is the most common cause.
  2. Test the raw endpoint with curl and a bearer token to inspect content type and first bytes.
  3. Bypass the proxy for gmail.googleapis.com.
  4. If persistent, capture the body prefix for diagnosis.
Defensive patterns

Strategy: validation

Validate before calling

let ct = resp.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("");
if !ct.contains("json") {
    return Err(anyhow::anyhow!("attachment endpoint returned '{ct}' — interception suspected"));
}

Type guard

fn is_attachment_envelope(v: &serde_json::Value) -> bool {
    v.get("data").map(|d| d.is_string()).unwrap_or(false)
        || v.get("size").and_then(|s| s.as_u64()).is_some()
}

Try / catch

let text = resp.text().await?;
let body: Value = serde_json::from_str(&text)
    .map_err(|e| anyhow::anyhow!("attachment body not JSON ({e}): {}", &text[..text.len().min(120)]))?;
if !is_attachment_envelope(&body) { return Err(anyhow::anyhow!("unexpected attachment envelope")); }

Prevention

When it happens

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

Common situations: Large-attachment forwards through TLS-inspecting proxies; captive portals mid-session.

Understand the failure class

Related errors


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