{"record":{"id":"64aaac22071d86ed","repo":"googleworkspace/cli","slug":"pub-sub-pull-failed-e","errorCode":null,"errorMessage":"Pub/Sub pull failed: {e}","messagePattern":"Pub/Sub pull failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/events/subscribe.rs","lineNumber":408,"sourceCode":"            .map_err(|e| GwsError::Auth(format!(\"Failed to get Pub/Sub token: {e}\")))?;\n        let pull_body = json!({\n            \"maxMessages\": config.max_messages,\n        });\n\n        let pull_future = client\n            .post(format!(\"{pubsub_api_base}/{subscription}:pull\"))\n            .bearer_auth(&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(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\").into()),\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":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/events/subscribe.rs#L390-L426","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before: any non-timeout transport error kills the whole subscribe session\nErr(e) => return Err(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\").into()),\n\n// after: tolerate transient connection resets by skipping a bounded number of failures\nErr(e) if e.is_connect() || e.is_request() => {\n    consecutive_errors += 1;\n    if consecutive_errors > 10 {\n        return Err(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\").into());\n    }\n    tokio::time::sleep(std::time::Duration::from_secs(2)).await;\n    continue;\n}\nErr(e) => return Err(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\").into()),","handlingStrategy":"retry","validationCode":"// Pre-flight before entering a long subscribe session\nasync fn ensure_pubsub_reachable(client: &reqwest::Client) -> Result<(), String> {\n    let resp = client\n        .get(\"https://pubsub.googleapis.com/\")\n        .timeout(std::time::Duration::from_secs(10))\n        .send()\n        .await\n        .map_err(|e| format!(\"Pub/Sub unreachable: {e}\"))?;\n    Ok(()) // any HTTP response (even 404) proves transport works\n}","typeGuard":null,"tryCatchPattern":"// Treat transport errors in the pull loop as retryable-with-backoff, not fatal:\nmatch result {\n    Ok(r) => r,\n    Err(e) if e.is_timeout() => continue,\n    Err(e) if e.is_connect() => { backoff_and_continue(e); }\n    Err(e) => return Err(anyhow::anyhow!(\"Pub/Sub pull failed: {e}\").into()),\n}","preventionTips":["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."],"tags":["pubsub","network","long-polling","reqwest","events-subscribe"],"backgroundTag":"http-request-failed","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}