{"record":{"id":"79c95672b00f4256","repo":"Hmbown/CodeWhale","slug":"name-oauth-operation-failed-permanently-err-sign-in-again","errorCode":null,"errorMessage":"{name} OAuth {operation} failed permanently ({err}). Sign in again with `{}`.","messagePattern":"(.+?) OAuth (.+?) failed permanently \\((.+?)\\)\\. Sign in again with `(.+?)`\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":986,"sourceCode":"    body: &str,\n    operation: &str,\n    params: &OAuthProviderParams,\n) -> Result<OAuthTokenMaterial> {\n    let name = params.display_name;\n    let parsed: OAuthTokenMaterial = serde_json::from_str(body).map_err(|_| {\n        anyhow::anyhow!(\"{name} OAuth {operation} returned HTTP {status} that was not token JSON\")\n    })?;\n    if !(200..300).contains(&status) || parsed.error.is_some() {\n        let err = parsed.error.as_deref().unwrap_or(\"token_error\");\n        if matches!(\n            err,\n            \"invalid_grant\"\n                | \"refresh_token_reused\"\n                | \"refresh_token_expired\"\n                | \"refresh_token_invalidated\"\n        ) || status == 401\n        {\n            bail!(\n                \"{name} OAuth {operation} failed permanently ({err}). Sign in again with `{}`.\",\n                params.relogin_hint\n            );\n        }\n        bail!(\"{name} OAuth {operation} failed ({err})\");\n    }\n    anyhow::ensure!(\n        parsed\n            .access_token\n            .as_deref()\n            .is_some_and(|token| !token.trim().is_empty()),\n        \"{name} OAuth {operation} returned an empty access token\"\n    );\n    Ok(parsed)\n}\n\nfn compact_form_error(body: &str) -> String {\n    body.chars().filter(|c| !c.is_control()).take(80).collect()","sourceCodeStart":968,"sourceCodeEnd":1004,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L968-L1004","documentation":"Thrown when an OAuth refresh/exchange operation fails with a permanently unrecoverable condition: the token endpoint returned `invalid_grant` (or a provider-specific refresh-token-reused/expired/invalidated error) or HTTP 401. The message includes the provider name, operation, underlying error, and the exact command to sign in again.","triggerScenarios":"Calling the refresh/exchange path when the stored refresh token is expired, revoked, already used (reuse detection), or otherwise rejected — detected via error code match or HTTP status 401 in the token response.","commonSituations":"Refresh token rotated elsewhere (second client consumed it); long-idle session exceeded the provider's refresh-token lifetime; admin revoked the session; provider reuse-detection invalidated the token after a duplicate refresh.","solutions":["Sign in again using the printed relogin command/hint to obtain fresh tokens","Stop retrying with the old refresh token — reuse attempts trigger further invalidation","If this happens repeatedly across machines, ensure only one client refreshes at a time (token rotation conflicts)","Check provider session/consent revocation in the provider's admin/user dashboard"],"exampleFix":"// before\nlet token = refresh_access_token(provider, stored_refresh)?; // permanent failure\n// after\nlet token = match refresh_access_token(provider, stored_refresh) {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"failed permanently\") => {\n        eprintln!(\"credentials stale; running interactive re-login\");\n        run_login(provider).await?;\n        refresh_access_token(provider, freshly_stored_refresh(provider))?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"try-catch","validationCode":"// detect a stale refresh token before it poisons the flow\nif (refreshTokenAge(provider) > maxRefreshTokenLifetime) {\n  triggerReLogin(provider);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await refreshAccessToken(provider);\n} catch (e) {\n  if (String(e).includes('failed permanently')) {\n    await interactiveReLogin(provider); // tokens are unrecoverable\n  } else {\n    retryWithBackoff(e);\n  }\n}","preventionTips":["Never retry after a permanent refresh failure — further attempts trigger reuse-detection","Ensure only one client/process refreshes a given token at a time","Refresh proactively before token expiry to avoid revocation surprises","Surface the printed relogin command to users instead of swallowing the error"],"tags":["oauth","token-refresh","authentication"],"backgroundTag":"jwt-token-expired","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}