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
- Fix the provider/token endpoint so it returns a real non-empty access_token
- Check whether the server returned a token error without an `error` field — inspect the raw response with curl
- 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
- Verify the provider conforms to RFC 6749 token responses before integration
- Capture and inspect a raw token response during provider onboarding
- If using a stub/mock server, always populate access_token in fixtures
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
- Codewhale-owned xAI OAuth file
- returned an unusable verification URI
- invalid Codewhale-owned ChatGPT OAuth generation; expected…
- invalid Codewhale-owned xAI OAuth generation; expected…
- MCP server URL ' ' must include a host
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)