zeroclaw-labs/zeroclaw · critical
LinkedIn token refresh failed ({}): {}
Error message
LinkedIn token refresh failed ({}): {} What it means
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.
Source
Thrown at crates/zeroclaw-tools/src/linkedin_client.rs:567
})?;
let client = Self::client();
let response = client
.post(LINKEDIN_OAUTH_TOKEN_URL)
.form(&[
("grant_type", "refresh_token"),
("refresh_token", refresh),
("client_id", &creds.client_id),
("client_secret", &creds.client_secret),
])
.send()
.await
.context("LinkedIn token refresh request failed")?;
let status = response.status();
if !status.is_success() {
let body_text = response.text().await.unwrap_or_default();
anyhow::bail!("LinkedIn token refresh failed ({}): {}", status, body_text);
}
let json: serde_json::Value = response
.json()
.await
.context("Failed to parse token refresh response")?;
let new_token = json
.get("access_token")
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| {
::zeroclaw_log::record!(
ERROR,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({"field": "access_token"})),
"linkedin_client: token refresh response missing access_token"View on GitHub (pinned to 88bb9c8533)
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.
Example fix
// before: refresh token revoked by user -> LinkedIn token refresh failed (400 Bad Request): {..."invalid_grant"}
let token = client.api_request(Method::GET, &url, &creds.access_token, None).await?;
// after: detect dead refresh and fall back to interactive re-auth
match client.api_request(Method::GET, &url, &creds.access_token, None).await {
Err(e) if e.to_string().contains("token refresh failed") => {
// trigger the OAuth authorize flow again; do not loop retrying refresh
return Err(e).context("re-authorization required");
}
other => other,
} Defensive patterns
Strategy: try-catch
Try / catch
// Never loop on refresh failures: invalid_grant is permanent
match client.refresh_token(&creds).await {
Ok(new_access) => Ok(new_access),
Err(e) => {
let m = e.to_string();
if m.contains("invalid_grant") {
// refresh token revoked/expired: require interactive re-authorization
Err(e).context("LinkedIn re-authorization required")
} else if m.contains("invalid_client") {
Err(e).context("client_id/client_secret mismatch")
} else { Err(e) }
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- token refresh failed ({status}): {body}
- OAuth refresh failed (HTTP {status}): {detail}
- MiniMax OAuth refresh failed (HTTP {status}): {detail}
- LinkedIn get_profile failed ({}): {}
- Google device code request failed ({}): {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/75949fd187a389c2.
Report an issue: GitHub.