{"record":{"id":"44189c730d848d19","repo":"googleworkspace/cli","slug":"5-44189c","errorCode":"5","errorMessage":"Pub/Sub pull failed: {e}","messagePattern":"Pub/Sub pull failed: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/watch.rs","lineNumber":288,"sourceCode":"            .access_token()\n            .await\n            .context(\"Failed to get Pub/Sub token\")?;\n        let pull_body = json!({ \"maxMessages\": config.max_messages });\n        let pull_future = runtime\n            .client\n            .post(format!(\"{}/{subscription}:pull\", runtime.pubsub_api_base))\n            .bearer_auth(&pubsub_token)\n            .header(\"Content-Type\", \"application/json\")\n            .json(&pull_body)\n            .timeout(std::time::Duration::from_secs(config.poll_interval.max(10)))\n            .send();\n\n        let resp = tokio::select! {\n            result = pull_future => {\n                match result {\n                    Ok(r) => r,\n                    Err(e) if e.is_timeout() => continue,\n                    Err(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\"))),\n                }\n            }\n            _ = super::super::shutdown_signal() => {\n                eprintln!(\"\\nReceived shutdown signal, stopping...\");\n                return Ok(());\n            }\n        };\n\n        if !resp.status().is_success() {\n            let body = resp.text().await.unwrap_or_default();\n            return Err(GwsError::Api {\n                code: 400,\n                message: format!(\"Pub/Sub pull failed: {body}\"),\n                reason: \"pubsubError\".to_string(),\n                enable_url: None,\n            });\n        }\n","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/watch.rs#L270-L306","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (watch.rs): any non-timeout transport error kills the loop\nErr(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\"))),\n\n// after: also tolerate transient connection errors like the existing timeout branch\nErr(e) if e.is_timeout() || e.is_connect() || e.is_request() => {\n    eprintln!(\"transient pull error ({e}), retrying...\");\n    tokio::time::sleep(std::time::Duration::from_secs(1)).await;\n    continue;\n}\nErr(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\"))),","handlingStrategy":"retry","validationCode":"// Before starting the watch loop, verify Pub/Sub reachability\nasync fn pubsub_reachable(client: &reqwest::Client) -> bool {\n    client\n        .get(\"https://pubsub.googleapis.com/\")\n        .timeout(std::time::Duration::from_secs(5))\n        .send()\n        .await\n        .map(|r| r.status().as_u16() < 500 || r.status().as_u16() >= 400 /* any HTTP answer = transport OK */)\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// Treat transport errors as backoff-and-continue, like the existing timeout branch\nmatch result {\n    Ok(r) => r,\n    Err(e) if e.is_timeout() => continue,\n    Err(e) if e.is_connect() => {\n        tokio::time::sleep(std::time::Duration::from_secs(2)).await;\n        continue; // network hiccup — do not kill a watch loop that may be hours in\n    }\n    Err(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\"))),\n}","preventionTips":["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."],"tags":["network","pubsub","gmail-watch","reqwest","transport-error"],"backgroundTag":"http-connection-error","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}