{"record":{"id":"36cb16fb051a5a0e","repo":"zeroclaw-labs/zeroclaw","slug":"token-refresh-failed-status-body","errorCode":null,"errorMessage":"token refresh failed ({status}): {body}","messagePattern":"token refresh failed \\((.+?)\\): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"crates/zeroclaw-channels/src/reddit.rs","lineNumber":118,"sourceCode":"        let client = self.http_client();\n        let resp = client\n            .post(REDDIT_TOKEN_URL)\n            .basic_auth(&self.client_id, Some(&self.client_secret))\n            .header(\"User-Agent\", USER_AGENT)\n            .form(&[\n                (\"grant_type\", \"refresh_token\"),\n                (\"refresh_token\", &self.refresh_token),\n            ])\n            .send()\n            .await?;\n\n        let status = resp.status();\n        if !status.is_success() {\n            let body = resp\n                .text()\n                .await\n                .unwrap_or_else(|e| format!(\"<failed to read response: {e}>\"));\n            bail!(\"token refresh failed ({status}): {body}\");\n        }\n\n        let token_resp: RedditTokenResponse = resp.json().await?;\n        let mut auth = self.auth.lock();\n        auth.access_token = token_resp.access_token;\n        auth.expires_at =\n            Instant::now() + Duration::from_secs(token_resp.expires_in.saturating_sub(60));\n        Ok(())\n    }\n\n    /// Get a valid access token, refreshing if expired.\n    async fn get_access_token(&self) -> Result<String> {\n        {\n            let auth = self.auth.lock();\n            if !auth.access_token.is_empty() && Instant::now() < auth.expires_at {\n                return Ok(auth.access_token.clone());\n            }\n        }","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/reddit.rs#L100-L136","documentation":"Raised by RedditChannel::refresh_access_token when POST https://www.reddit.com/api/v1/access_token (grant_type=refresh_token, HTTP Basic client_id:client_secret) returns a non-2xx status. The response body is included verbatim — Reddit returns JSON like {'error': 'invalid_grant'} for revoked/invalid refresh tokens or 401 invalid_client for a bad app credential pair. Every Reddit operation funnels through this: get_access_token, listen polling, fetch_inbox, mark_read and send all fail when it breaks.","triggerScenarios":"Any Reddit API call with an empty/expired cached access token triggers refresh_access_token; a 400 invalid_grant (refresh token revoked or mistyped), 401 (wrong client_id/client_secret for the app that issued the token), 429 (IP or app rate limited), or 5xx triggers the bail before token_resp parsing.","commonSituations":"User revoked app access in Reddit settings, invalidating the stored refresh token; refresh_token issued by one Reddit app but client_id/secret from another; placeholder or truncated secrets in config; exceeding Reddit's 60 req/min budget so token endpoint also rate limits; datacenter IP blocked by Reddit.","solutions":["If the body says invalid_grant or status is 400: the refresh token is dead — re-run the OAuth flow for the Reddit app and replace the stored refresh_token","If 401/invalid_client: the client_id/client_secret pair does not match the app that issued the refresh token — fix the config","If 429: back off and lower polling rate (Reddit enforces 60 requests/minute; POLL_INTERVAL is 5s)","Verify the configured User-Agent — Reddit rejects generic/default agents","Check for a second process using the same credentials doubling the request rate"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match channel.get_access_token().await {\n    Err(err) => {\n        let msg = format!(\"{err:#}\");\n        if msg.starts_with(\"token refresh failed (400\") || msg.starts_with(\"token refresh failed (401\") {\n            return Err(anyhow!(\"Reddit credentials need manual re-auth: {msg}\")).context(\"stop retrying\"); // permanent\n        }\n        tokio::time::sleep(Duration::from_secs(30)).await; // 429/5xx: transient\n        channel.get_access_token().await\n    }\n    ok => ok,\n}","preventionTips":["Never retry 400/401 refresh failures — invalid_grant means the refresh token is revoked and only re-consent fixes it","Keep client_id/client_secret/refresh_token from the same Reddit app in config","Stay under Reddit's 60 req/min budget; the token endpoint rate-limits too","Alert on refresh failures distinctly from API failures — they indicate credential rot, not load"],"tags":["reddit","oauth","refresh-token","authentication","http-status","channel"],"backgroundTag":"oauth-refresh-token-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}