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

  1. Re-run the forward/send — attachment fetch is per-part and restartable.
  2. Stabilize the connection or switch networks before retrying large downloads.
  3. Use `--attach` size limits / avoid `--attach-original` for huge parts if the network is unreliable.
  4. 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

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


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