{"record":{"id":"75949fd187a389c2","repo":"zeroclaw-labs/zeroclaw","slug":"linkedin-token-refresh-failed","errorCode":null,"errorMessage":"LinkedIn token refresh failed ({}): {}","messagePattern":"LinkedIn token refresh failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/zeroclaw-tools/src/linkedin_client.rs","lineNumber":567,"sourceCode":"            })?;\n\n        let client = Self::client();\n        let response = client\n            .post(LINKEDIN_OAUTH_TOKEN_URL)\n            .form(&[\n                (\"grant_type\", \"refresh_token\"),\n                (\"refresh_token\", refresh),\n                (\"client_id\", &creds.client_id),\n                (\"client_secret\", &creds.client_secret),\n            ])\n            .send()\n            .await\n            .context(\"LinkedIn token refresh request failed\")?;\n\n        let status = response.status();\n        if !status.is_success() {\n            let body_text = response.text().await.unwrap_or_default();\n            anyhow::bail!(\"LinkedIn token refresh failed ({}): {}\", status, body_text);\n        }\n\n        let json: serde_json::Value = response\n            .json()\n            .await\n            .context(\"Failed to parse token refresh response\")?;\n\n        let new_token = json\n            .get(\"access_token\")\n            .and_then(|v| v.as_str())\n            .map(String::from)\n            .ok_or_else(|| {\n                ::zeroclaw_log::record!(\n                    ERROR,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                        .with_attrs(::serde_json::json!({\"field\": \"access_token\"})),\n                    \"linkedin_client: token refresh response missing access_token\"","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/linkedin_client.rs#L549-L585","documentation":"Thrown when the OAuth refresh-token grant to LINKEDIN_OAUTH_TOKEN_URL returns non-2xx, body included. refresh_token is invoked from api_request when the access token needs renewal; failure here means the whole API call chain fails with this error. The body for 400 typically contains error=invalid_grant or invalid_client, pinpointing whether the refresh token or the client credentials are bad.","triggerScenarios":"Refresh token expired (LinkedIn refresh tokens live ~1 year but expire sooner if unused 6 months) or revoked by the user (400 invalid_grant); wrong client_id/client_secret pair (400 invalid_client); refresh token belonging to a different app; member re-authorized the app, invalidating the stored refresh token.","commonSituations":"Long-lived integrations that idle past the refresh-token inactivity window; copying tokens between a dev and prod app; user revoking app access from LinkedIn settings; rotating the app secret without updating stored credentials.","solutions":["For invalid_grant (400), the refresh token is dead — re-run the full OAuth authorization flow to get fresh tokens.","For invalid_client (400), verify client_id and client_secret match the app that issued the refresh token (and that the secret was not rotated).","Refresh proactively (e.g. scheduled) so the refresh token never idles past its inactivity expiry.","Persist the NEW refresh token if the response includes one, so future refreshes keep working."],"exampleFix":"// before: refresh token revoked by user -> LinkedIn token refresh failed (400 Bad Request): {...\"invalid_grant\"}\nlet token = client.api_request(Method::GET, &url, &creds.access_token, None).await?;\n\n// after: detect dead refresh and fall back to interactive re-auth\nmatch client.api_request(Method::GET, &url, &creds.access_token, None).await {\n    Err(e) if e.to_string().contains(\"token refresh failed\") => {\n        // trigger the OAuth authorize flow again; do not loop retrying refresh\n        return Err(e).context(\"re-authorization required\");\n    }\n    other => other,\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Never loop on refresh failures: invalid_grant is permanent\nmatch client.refresh_token(&creds).await {\n    Ok(new_access) => Ok(new_access),\n    Err(e) => {\n        let m = e.to_string();\n        if m.contains(\"invalid_grant\") {\n            // refresh token revoked/expired: require interactive re-authorization\n            Err(e).context(\"LinkedIn re-authorization required\")\n        } else if m.contains(\"invalid_client\") {\n            Err(e).context(\"client_id/client_secret mismatch\")\n        } else { Err(e) }\n    }\n}","preventionTips":["Persist any new refresh token returned by each refresh; losing it breaks the chain.","Refresh on a schedule well before expiry so refresh tokens never idle past the inactivity window.","Keep client_id, client_secret, and tokens from the same LinkedIn app; rotate secrets deliberately and together.","When users revoke access, expect invalid_grant and route them through re-consent."],"tags":["linkedin","oauth","refresh-token","http-status"],"backgroundTag":"refresh-token-expired","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}