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
- Check basic connectivity: `curl -sS https://pubsub.googleapis.com/` (any HTTP response means transport works).
- If a proxy is configured, verify HTTPS_PROXY/HTTP_PROXY/NO_PROXY values and that the proxy is reachable.
- 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).
- 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
- Run watch sessions inside tmux/screen so a network blip plus loop exit doesn't lose the terminal history with the reconnection info.
- Always launch with --no-cleanup off (default) or note the printed subscription name so an aborted loop can reconnect with --subscription.
- Export NO_PROXY/HTTPS_PROXY deliberately in the environment that runs gws.
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
- Pub/Sub pull failed: {e}
- 5
- Failed to fetch message: {e}
- Failed to fetch sendAs settings: {e}
- People API request failed: {e}
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/44189c730d848d19.
Report an issue: GitHub.