googleworkspace/cli · error · GwsError
Failed to fetch attachment: {e}
Error message
Failed to fetch attachment: {e} What it means
Transport-level failure while downloading an attachment: `send_with_retry` on `GET /gmail/v1/users/me/messages/{messageId}/attachments/{attachmentId}` never produced an HTTP response after retries. Both path segments are percent-encoded via `encode_path_segment`, so URL shape is safe; the failure is DNS/connect/TLS/transport. HTTP error statuses become `build_api_error` instead.
Source
Thrown at crates/google-workspace-cli/src/helpers/gmail/mod.rs:702
/// Fetch binary data for a single attachment from the Gmail API.
///
/// Calls `GET /users/me/messages/{messageId}/attachments/{attachmentId}`,
/// decodes the base64url `data` field, and returns raw bytes.
async fn fetch_attachment_data(
client: &reqwest::Client,
token: &str,
message_id: &str,
attachment_id: &str,
) -> Result<Vec<u8>, GwsError> {
let url = format!(
"https://gmail.googleapis.com/gmail/v1/users/me/messages/{}/attachments/{}",
crate::validate::encode_path_segment(message_id),
crate::validate::encode_path_segment(attachment_id),
);
let resp = crate::client::send_with_retry(|| client.get(&url).bearer_auth(token))
.await
.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}")))?;View on GitHub (pinned to a3768d0e82)
Solutions
- Re-run the forward/send — attachment fetch is per-part and restartable.
- Stabilize the connection or switch networks before retrying large downloads.
- Use `--attach` size limits / avoid `--attach-original` for huge parts if the network is unreliable.
- Check proxy rules for the `/attachments/` URL path.
Defensive patterns
Strategy: retry
Validate before calling
// Before --attach-original flows, confirm attachment endpoints are reachable and the part still exists
let part = find_part_by_attachment_id(&msg, attachment_id);
if part.is_none() { return Err(anyhow::anyhow!("attachment {attachment_id} no longer listed")); } Try / catch
// Attachment downloads are per-part: retry the failing part, not the whole message fetch
for attempt in 0..3 {
match fetch_attachment(client, token, message_id, attachment_id).await {
Ok(bytes) => return Ok(bytes),
Err(GwsError::Other(_)) if attempt < 2 => tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await,
Err(e) => return Err(e),
}
} Prevention
- Re-fetch message metadata before downloading attachments to confirm attachmentIds are still valid.
- Retry per-part with exponential backoff; parts are independent.
- Avoid --attach-original for very large parts on unreliable links.
When it happens
Trigger: Downloading a large attachment over a dropping connection until retries exhaust; offline `+forward --attach-original`; proxy blocking the attachments path pattern.
Common situations: Forwarding messages with big attachments on hotel wifi; CI jobs pulling attachments where egress is rate-limited; mobile tethering.
Related errors
- Failed to fetch message: {e}
- Failed to fetch sendAs settings: {e}
- People API request failed: {e}
- Failed to fetch user profile: {e}
- Pub/Sub pull failed: {e}
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/cd8ff69063f49b08.
Report an issue: GitHub.