googleworkspace/cli · error · GwsError

5

5

Error message

Pub/Sub pull failed: {e}

What it means

Thrown when the reqwest POST to the Pub/Sub `subscriptions:pull` endpoint fails at the transport layer with a non-timeout error (connection refused/reset, DNS resolution failure, TLS handshake failure, malformed proxy response). The watch loop in `gmail +watch` explicitly tolerates timeouts (`Err(e) if e.is_timeout() => continue`) but aborts the whole loop for any other reqwest error, wrapping it in `GwsError::Other` with exit code 5.

Source

Thrown at crates/google-workspace-cli/src/helpers/gmail/watch.rs:288

            .access_token()
            .await
            .context("Failed to get Pub/Sub token")?;
        let pull_body = json!({ "maxMessages": config.max_messages });
        let pull_future = runtime
            .client
            .post(format!("{}/{subscription}:pull", runtime.pubsub_api_base))
            .bearer_auth(&pubsub_token)
            .header("Content-Type", "application/json")
            .json(&pull_body)
            .timeout(std::time::Duration::from_secs(config.poll_interval.max(10)))
            .send();

        let resp = tokio::select! {
            result = pull_future => {
                match result {
                    Ok(r) => r,
                    Err(e) if e.is_timeout() => continue,
                    Err(e) => return Err(GwsError::Other(anyhow::anyhow!("Pub/Sub pull failed: {e}"))),
                }
            }
            _ = super::super::shutdown_signal() => {
                eprintln!("\nReceived shutdown signal, stopping...");
                return Ok(());
            }
        };

        if !resp.status().is_success() {
            let body = resp.text().await.unwrap_or_default();
            return Err(GwsError::Api {
                code: 400,
                message: format!("Pub/Sub pull failed: {body}"),
                reason: "pubsubError".to_string(),
                enable_url: None,
            });
        }

View on GitHub (pinned to a3768d0e82)

Solutions

  1. Check basic connectivity: `curl -sS https://pubsub.googleapis.com/` (any HTTP response means transport works).
  2. If a proxy is configured, verify HTTPS_PROXY/HTTP_PROXY/NO_PROXY values and that the proxy is reachable.
  3. Re-run `gws gmail +watch --subscription <name>` using the reconnection info printed by the previous run (no cleanup ran on abort, the subscription still exists).
  4. For private clusters/VPCs, confirm Private Google Access is enabled for the subnet egress to googleapis.com.

Example fix

// before (watch.rs): any non-timeout transport error kills the loop
Err(e) => return Err(GwsError::Other(anyhow::anyhow!("Pub/Sub pull failed: {e}"))),

// after: also tolerate transient connection errors like the existing timeout branch
Err(e) if e.is_timeout() || e.is_connect() || e.is_request() => {
    eprintln!("transient pull error ({e}), retrying...");
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    continue;
}
Err(e) => return Err(GwsError::Other(anyhow::anyhow!("Pub/Sub pull failed: {e}"))),
Defensive patterns

Strategy: retry

Validate before calling

// Before starting the watch loop, verify Pub/Sub reachability
async fn pubsub_reachable(client: &reqwest::Client) -> bool {
    client
        .get("https://pubsub.googleapis.com/")
        .timeout(std::time::Duration::from_secs(5))
        .send()
        .await
        .map(|r| r.status().as_u16() < 500 || r.status().as_u16() >= 400 /* any HTTP answer = transport OK */)
        .unwrap_or(false)
}

Try / catch

// Treat transport errors as backoff-and-continue, like the existing timeout branch
match result {
    Ok(r) => r,
    Err(e) if e.is_timeout() => continue,
    Err(e) if e.is_connect() => {
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        continue; // network hiccup — do not kill a watch loop that may be hours in
    }
    Err(e) => return Err(GwsError::Other(anyhow::anyhow!("Pub/Sub pull failed: {e}"))),
}

Prevention

When it happens

Trigger: Running `gws gmail +watch` (or `--subscription <name>`) while the machine loses network connectivity mid-poll; a corporate proxy returning a broken CONNECT response; DNS for `pubsub.googleapis.com` (or a custom `pubsub_api_base`) failing to resolve; a VPN drop resetting the long-lived TCP connection between polls.

Common situations: Laptops that sleep/roam networks while a watch session runs; containers with flaky egress; HTTPS_PROXY/http_proxy env vars pointing at a dead proxy; air-gapped or private-cluster environments without Private Google Access to googleapis.com.

Related errors


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