zeroclaw-labs/zeroclaw · error · anyhow::Error
Missing OAuth state in callback
Error message
Missing OAuth state in callback
What it means
Thrown when `parse_code_from_redirect` was given an `expected_state`, the input looks like a real callback (has `?`, or carries `code`/`state`/`error` params), but the query has no `state` parameter at all. The library refuses to process a callback that cannot prove it originated from your authorize request. Raw-code input without callback shape is still accepted; only callback-shaped payloads must carry state.
Source
Thrown at crates/zeroclaw-providers/src/auth/openai_oauth.rs:325
|| params.contains_key("code")
|| params.contains_key("state")
|| params.contains_key("error");
if let Some(err) = params.get("error") {
let desc = params
.get("error_description")
.cloned()
.unwrap_or_else(|| "OAuth authorization failed".to_string());
anyhow::bail!("OpenAI OAuth error: {err} ({desc})");
}
if let Some(expected_state) = expected_state {
if let Some(got) = params.get("state") {
if got != expected_state {
anyhow::bail!("OAuth state mismatch");
}
} else if is_callback_payload {
anyhow::bail!("Missing OAuth state in callback");
}
}
if let Some(code) = params.get("code").cloned() {
return Ok(code);
}
if !is_callback_payload {
return Ok(trimmed.to_string());
}
anyhow::bail!("Missing OAuth code in callback")
}
pub fn extract_account_id_from_jwt(token: &str) -> Option<String> {
let payload = token.split('.').nth(1)?;
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload)View on GitHub (pinned to 88bb9c8533)
Solutions
- Build the authorize URL with `build_authorize_url(&pkce)` — it always sets `state` (and PKCE fields)
- Restart the flow and let the browser complete the full redirect; never hand-type the callback URL
- Keep the redirect's query string intact — no rewriting middleware that drops parameters
- If parsing redirects yourself, pass the state value unchanged from the authorize request
Example fix
// before: hand-built authorize URL without state
let url = format!("{}?response_type=code&client_id={}", OPENAI_OAUTH_AUTHORIZE_URL, OPENAI_OAUTH_CLIENT_ID);
// callback arrives with no state -> "Missing OAuth state in callback"
// after: helper embeds state and PKCE correctly
let pkce = generate_pkce_state();
let url = build_authorize_url(&pkce); Defensive patterns
Strategy: validation
Validate before calling
fn callback_has_state(input: &str) -> bool {
input.split_once('?').map_or(false, |(_, q)| {
q.split('&').any(|pair| pair.starts_with("state="))
})
}
if callback_has_state(path) {
let code = parse_code_from_redirect(path, Some(&pkce.state))?;
} Try / catch
match parse_code_from_redirect(path, Some(&pkce.state)) {
Ok(code) => exchange(code),
Err(e) if e.to_string().contains("Missing OAuth state") => restart_login_flow().await,
Err(e) => return Err(e),
} Prevention
- Always build the authorize URL with build_authorize_url — it embeds state
- Never strip query parameters from the redirect URI
- Keep the loopback listener on the exact registered redirect URI so the browser round-trips state
When it happens
Trigger: `parse_code_from_redirect("/auth/callback?code=abc", Some("xyz"))` — a `code` param present but no `state` — reaching the `else if is_callback_payload` branch. Via `receive_loopback_code` when the redirect from auth.openai.com arrives without state because the authorize request never included one, or a callback-shaped request hits the listener from another source.
Common situations: Authorize URL hand-built instead of using `build_authorize_url` (which always embeds state); test fixtures or custom integrations constructing the callback path by hand; a proxy or manual edit stripping query parameters from the redirect.
Related errors
- OAuth state mismatch
- Missing OAuth code in callback
- OpenAI OAuth token request failed ({status}): {body}
- {err}: {desc}
- xAI OAuth state mismatch
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/23d4c406ec13402c.
Report an issue: GitHub.