googleworkspace/cli · error

Pub/Sub pull failed: {e}

Error message

Pub/Sub pull failed: {e}

What it means

Thrown inside the long-poll loop of `gws events +subscribe` when the reqwest POST to `{pubsub_api_base}/{subscription}:pull` fails with a transport-level error that is NOT a timeout. Timeouts are explicitly swallowed (`Err(e) if e.is_timeout() => continue`), so any other reqwest error (DNS, connection refused/reset, TLS, proxy, malformed request) terminates the subscribe session and triggers the cleanup path that deletes the Pub/Sub topic and subscription.

Source

Thrown at crates/google-workspace-cli/src/helpers/events/subscribe.rs:408

            .map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?;
        let pull_body = json!({
            "maxMessages": config.max_messages,
        });

        let pull_future = client
            .post(format!("{pubsub_api_base}/{subscription}:pull"))
            .bearer_auth(&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(anyhow::anyhow!("Pub/Sub pull failed: {e}").into()),
                }
            }
            _ = 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/` from the same machine/network.
  2. If behind a proxy, verify HTTPS_PROXY/SOCKS settings and that the proxy allows long-lived POST requests (poll interval is at least 10s).
  3. Re-run `gws events +subscribe` — cleanup already deleted the old topic/subscription, so a fresh session recreates them.
  4. If it recurs on flaky networks, raise `--poll-interval` so each pull has a larger per-request timeout budget.
  5. Inspect stderr: the reqwest error string distinguishes dns/connect/tls causes.

Example fix

// before: any non-timeout transport error kills the whole subscribe session
Err(e) => return Err(anyhow::anyhow!("Pub/Sub pull failed: {e}").into()),

// after: tolerate transient connection resets by skipping a bounded number of failures
Err(e) if e.is_connect() || e.is_request() => {
    consecutive_errors += 1;
    if consecutive_errors > 10 {
        return Err(anyhow::anyhow!("Pub/Sub pull failed: {e}").into());
    }
    tokio::time::sleep(std::time::Duration::from_secs(2)).await;
    continue;
}
Err(e) => return Err(anyhow::anyhow!("Pub/Sub pull failed: {e}").into()),
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight before entering a long subscribe session
async fn ensure_pubsub_reachable(client: &reqwest::Client) -> Result<(), String> {
    let resp = client
        .get("https://pubsub.googleapis.com/")
        .timeout(std::time::Duration::from_secs(10))
        .send()
        .await
        .map_err(|e| format!("Pub/Sub unreachable: {e}"))?;
    Ok(()) // any HTTP response (even 404) proves transport works
}

Try / catch

// Treat transport errors in the pull loop as retryable-with-backoff, not fatal:
match result {
    Ok(r) => r,
    Err(e) if e.is_timeout() => continue,
    Err(e) if e.is_connect() => { backoff_and_continue(e); }
    Err(e) => return Err(anyhow::anyhow!("Pub/Sub pull failed: {e}").into()),
}

Prevention

When it happens

Trigger: Calling `gws events +subscribe` and losing network mid-poll; DNS resolution failure to pubsub.googleapis.com; corporate proxy rejecting the long-lived POST; TLS certificate issues behind a MITM proxy; connection reset by the load balancer between polls. Note the per-request timeout is `poll_interval.max(10)` seconds, so a slow network that exceeds it silently continues instead of erroring.

Common situations: Laptop sleep/resume while subscribed, VPN flapping, hotel/wifi captive portals, SOCKS/HTTPS proxy misconfiguration (reqwest built with `socks` feature), or a deleted subscription on the server side returning a non-2xx (that path is a separate GwsError::Api, but auth header failures surface as 401 JSON, not this error).

Related errors


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