Hmbown/CodeWhale · error
OAuth failed ( )
Error message
{name} OAuth {operation} failed ({err}) What it means
The generic (non-permanent) counterpart to the permanent-refresh error: thrown when an OAuth operation fails with a transient or otherwise unclassified error — status not 401 and error code not in the permanent set. It surfaces the provider name, operation, and underlying error without a re-login instruction, so callers may retry.
Solutions
- Retry after a short backoff — this failure class is transient by classification
- Back off further on 429 (rate limit) before retrying
- If retries persistently fail, check provider status page, then sign in again via the relogin hint
- Log the `err` detail to identify any misclassified permanent error
Example fix
// before
let token = refresh_access_token(provider, refresh)?; // no retry on transient failure
// after
let token = match refresh_access_token(provider, refresh) {
Ok(t) => t,
Err(e) if e.to_string().contains("failed permanently") => return Err(e),
Err(_) => {
tokio::time::sleep(Duration::from_secs(5)).await;
refresh_access_token(provider, refresh)?
}
}; Defensive patterns
Strategy: retry
Validate before calling
// pre-check network reachability to reduce transient failures
await fetch(tokenEndpoint, { method: 'HEAD' }).catch(() => warn('token endpoint unreachable')); Try / catch
try {
await refreshAccessToken(provider);
} catch (e) {
if (String(e).includes('failed permanently')) throw e;
await sleep(backoff); // transient: retry with backoff
return refreshAccessToken(provider);
} Prevention
- Apply exponential backoff with jitter on transient OAuth failures
- Cap retry attempts and then fall back to interactive re-login
- Log the `err` detail to distinguish rate limits from outages
When it happens
Trigger: Token refresh/exchange receiving a 5xx, 429, network timeout, or an OAuth error code not classified as permanent (e.g. `temporarily_unavailable`, `server_error`).
Common situations: Provider outage or rate limiting during refresh; transient network failure; brief provider-side hiccups during token exchange.
Related errors
- MCP server rejected the request with and refreshing the…
- OAuth failed permanently ( ). Sign in again with ` `.
- OAuth returned HTTP that was not token JSON
- OIDC discovery failed with HTTP
- OAuth callback must be GET
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/5bfd5bab11027eb1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:991
let parsed: OAuthTokenMaterial = serde_json::from_str(body).map_err(|_| {
anyhow::anyhow!("{name} OAuth {operation} returned HTTP {status} that was not token JSON")
})?;
if !(200..300).contains(&status) || parsed.error.is_some() {
let err = parsed.error.as_deref().unwrap_or("token_error");
if matches!(
err,
"invalid_grant"
| "refresh_token_reused"
| "refresh_token_expired"
| "refresh_token_invalidated"
) || status == 401
{
bail!(
"{name} OAuth {operation} failed permanently ({err}). Sign in again with `{}`.",
params.relogin_hint
);
}
bail!("{name} OAuth {operation} failed ({err})");
}
anyhow::ensure!(
parsed
.access_token
.as_deref()
.is_some_and(|token| !token.trim().is_empty()),
"{name} OAuth {operation} returned an empty access token"
);
Ok(parsed)
}
fn compact_form_error(body: &str) -> String {
body.chars().filter(|c| !c.is_control()).take(80).collect()
}
/// Refresh an owned token through the seam at an explicit token URL —
/// discovered when the provider row demands it, pinned otherwise. Refresh is
/// a Codewhale-owned credential operation only: external imports neverView on GitHub (pinned to 73e0f67d83)