Hmbown/CodeWhale · error
OAuth callback state did not match the pending login
Error message
OAuth callback state did not match the pending login
What it means
The browser-based OAuth flow binds a random `state` value to the pending login and requires the callback to echo it back; `accept_callback` compares them. A mismatch means the callback the library received belongs to a different (or forged) login attempt, so it refuses to accept the authorization code — this is the standard CSRF protection for the OAuth redirect flow.
Solutions
- Restart the sign-in flow from scratch and use only the browser tab/window it opens
- Discard stale callback URLs from previous attempts
- Check for another login in progress that replaced the pending state, and cancel it before retrying
Example fix
// before: callback URL reused from an earlier login (state stale)
accept_callback(current_state, parse_callback("http://localhost/callback?code=x&state=OLD"))?
// after: use the fresh callback from the current login
accept_callback(current_state, parse_callback(fresh_callback_url))? Defensive patterns
Strategy: try-catch
Validate before calling
fn callback_matches_pending(expected: &str, url: &Url) -> bool {
url.query_pairs().any(|(k, v)| k == "state" && v == expected)
} Try / catch
match accept_callback(expected_state, outcome) {
Ok(code) => code,
Err(e) if e.to_string().contains("state did not match") => {
// stale or foreign callback: restart the login flow cleanly
cancel_pending_login();
start_new_login()? // fresh state, fresh browser tab
}
Err(e) => return Err(e),
} Prevention
- Complete each sign-in in the tab the flow opened; discard old tabs
- Never run two concurrent logins against the same pending state
- Do not hand-modify or re-encode callback URLs
- If replaying callback URLs in tests, regenerate state each time
When it happens
Trigger: `accept_callback(expected_state, CallbackOutcome::Success { code, state })` receives a `state` that does not equal the `expected_state` recorded when the login started — e.g. two logins interleaved, a stale browser tab replaying an old callback, or a state parameter stripped/rewritten by a redirect.
Common situations: Starting a second sign-in before finishing the first, so the old tab's callback no longer matches the new pending state; a proxy or URL rewriter mangling the query string; pasting a callback URL from an earlier attempt.
Related errors
- OAuth error callback state did not match the pending login
- Codewhale-owned OAuth path escaped the credentials directory
- Codewhale-owned OAuth path has an invalid basename
- Codewhale-owned xAI OAuth DACL is not current-user-only
- Codewhale-owned xAI OAuth DACL must grant only one user
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/58411effba0a8550.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:1221
return Ok(CallbackOutcome::Error {
error,
description,
state,
});
}
let code = code
.filter(|c| !c.trim().is_empty())
.context("OAuth callback missing authorization code")?;
let state = state
.filter(|s| !s.trim().is_empty())
.context("OAuth callback missing state")?;
Ok(CallbackOutcome::Success { code, state })
}
pub fn accept_callback(expected_state: &str, outcome: CallbackOutcome) -> Result<String> {
match outcome {
CallbackOutcome::Success { code, state } => {
anyhow::ensure!(
state == expected_state,
"OAuth callback state did not match the pending login"
);
Ok(code)
}
CallbackOutcome::Error {
error,
description,
state,
} => {
if let Some(state) = state {
anyhow::ensure!(
state == expected_state,
"OAuth error callback state did not match the pending login"
);
}
let detail = description
.filter(|text| !text.trim().is_empty())View on GitHub (pinned to 73e0f67d83)