Hmbown/CodeWhale · error

OAuth returned an empty access token

Error message

{name} OAuth {operation} returned an empty access token

What it means

After the token response parses successfully, the library requires a non-empty `access_token`. If the server returned well-formed token JSON whose `access_token` is missing, null, or whitespace-only, this ensure! fires with the provider name and operation. This guards against providers that report success semantics while omitting the credential the library actually needs.

Solutions

  1. Fix the provider/token endpoint so it returns a real non-empty access_token
  2. Check whether the server returned a token error without an `error` field — inspect the raw response with curl
  3. If testing against a stub, populate `access_token` in the fixture

Example fix

// before: idiosyncratic response missing the token
{"token_type": "bearer", "expires_in": 3600}
// after: standard token response
{"access_token": "eyJ...", "token_type": "bearer", "expires_in": 3600}
Defensive patterns

Strategy: validation

Validate before calling

fn token_payload_has_access_token(v: &serde_json::Value) -> bool {
    v.get("access_token")
        .and_then(|t| t.as_str())
        .map_or(false, |s| !s.trim().is_empty())
}

Type guard

fn has_nonempty_access_token(m: &OAuthTokenMaterial) -> bool {
    m.access_token.as_deref().is_some_and(|t| !t.trim().is_empty())
}

Try / catch

match poll_device_grant(...) {
    Ok(m) if has_nonempty_access_token(&m) => m,
    Ok(_) => anyhow::bail!("provider returned empty access token"),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A token exchange/refresh/device-poll response deserializes as `OAuthTokenMaterial` but has `access_token: null` or `access_token: ""` (or only whitespace).

Common situations: A non-standard OAuth provider returning `{ "error": "..." }`-adjacent shapes without the error field populated; a partially implemented/idiosyncratic token endpoint; a mock or stub server used in development that returns empty token JSON.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/a4f29941ace89daa. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/oauth.rs:993

    })?;
    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
/// refresh.
pub(crate) fn refresh_access_token_via(

View on GitHub (pinned to 73e0f67d83)