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
- Retry once; transient truncation is the most common cause.
- Test the raw endpoint with curl and a bearer token to inspect content type and first bytes.
- Bypass the proxy for gmail.googleapis.com.
- 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
- Content-type check before json() catches interceptor HTML on large attachment responses.
- Retry once — mid-body truncation on big base64 payloads is common on bad links.
- Keep test fixtures shaped `{"data": "...", "size": N}`.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Failed to parse message: {e}
- Failed to parse sendAs response: {e}
- Failed to parse profile: {e}
- Failed to parse People API response: {e}
- Failed to parse calendar list: {e}
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/1bf78ff49d710493.
Report an issue: GitHub.