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
- Check basic connectivity: `curl -sS https://pubsub.googleapis.com/` from the same machine/network.
- If behind a proxy, verify HTTPS_PROXY/SOCKS settings and that the proxy allows long-lived POST requests (poll interval is at least 10s).
- Re-run `gws events +subscribe` — cleanup already deleted the old topic/subscription, so a fresh session recreates them.
- If it recurs on flaky networks, raise `--poll-interval` so each pull has a larger per-request timeout budget.
- 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
- Run a reachability pre-flight to pubsub.googleapis.com before starting `gws events +subscribe`.
- On unstable networks, size `--poll-interval` generously — the per-pull request timeout is max(poll_interval, 10s).
- Remember the error path deletes the topic/subscription: expect a fresh resource set on each restart instead of reusing names.
- Pin down proxy env vars (HTTPS_PROXY/ALL_PROXY) in the environment that launches gws.
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
- 5
- Failed to fetch message: {e}
- Failed to fetch sendAs settings: {e}
- People API request failed: {e}
- Failed to fetch attachment: {e}
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/64aaac22071d86ed.
Report an issue: GitHub.