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

  1. Retry after a short backoff — this failure class is transient by classification
  2. Back off further on 429 (rate limit) before retrying
  3. If retries persistently fail, check provider status page, then sign in again via the relogin hint
  4. 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

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


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 never

View on GitHub (pinned to 73e0f67d83)