decolua/9router · warning · Error
Windsurf callback state mismatch
Error message
Windsurf callback state mismatch
What it means
parseWindsurfCallback throws this when the callback carries a state parameter that does not equal the expectedState generated at flow start. State is CSRF protection: a mismatch means the callback may not belong to the session that initiated the OAuth flow. The library only enforces it when expectedState was supplied and a state param is present.
Source
Thrown at src/lib/oauth/providers/windsurf.js:44
const text = String(raw || "").trim();
let queryStr = text;
if (text.includes("?")) queryStr = text.slice(text.indexOf("?") + 1);
if (text.startsWith("#")) queryStr = text.slice(1);
const params = Object.fromEntries(new URLSearchParams(queryStr));
const pick = (keys) => {
for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }
return null;
};
const err = pick(["error"]);
if (err) {
const desc = pick(["error_description"]);
throw new Error(desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf auth failed: ${err}`);
}
const accessToken = pick(["access_token", "token"]);
if (!accessToken) throw new Error("Windsurf callback missing access_token");
const state = pick(["state"]);
if (expectedState && state && state !== expectedState) {
throw new Error("Windsurf callback state mismatch");
}
return { firebaseIdToken: accessToken };
}
// POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}
async function fetchWindsurfRegisterUser(firebaseIdToken) {
const data = await windsurfSeatRequest(WINDSURF_CONFIG.registerApiBaseUrl, WINDSURF_CONFIG.registerPath, {
firebase_id_token: firebaseIdToken,
});
const apiKey = extractJsonPath(data, [["apiKey"], ["api_key"]]);
if (!apiKey) throw new Error("Windsurf RegisterUser missing apiKey");
const apiServerUrl = extractJsonPath(data, [["apiServerUrl"], ["api_server_url"]]) || WINDSURF_CONFIG.defaultApiServerUrl;
const name = extractJsonPath(data, [["name"]]);
return { apiKey, apiServerUrl, name };
}
// Best-effort: GetOneTimeAuthToken → GetCurrentUser → email/name.
async function fetchWindsurfUserInfo(apiServerUrl, firebaseIdToken) {View on GitHub (pinned to 90b52e06ff)
Solutions
- Restart the OAuth flow cleanly: open the newly generated auth URL and use only ITS redirect callback
- Discard stale tabs / previous authorization URLs; never paste a URL from an earlier attempt
- Ensure only one Windsurf connect flow is active at a time (single callback listener/port)
- If state was genuinely lost (e.g. a proxy dropped it), verify the token via RegisterUser and treat the flow as unvalidated at your own risk
Example fix
// before: reusing an old callback URL from a previous session
const tokens = await windsurf.exchangeToken(cfg, oldCallbackUrl, redirectUri, null, currentState);
// after: generate a fresh flow each time
const state = crypto.randomBytes(16).toString('hex');
const authUrl = windsurf.buildAuthUrl(cfg, redirectUri, state);
// ... open authUrl, receive callback for THIS state ...
const tokens = await windsurf.exchangeToken(cfg, freshCallbackUrl, redirectUri, null, state); Defensive patterns
Strategy: validation
Validate before calling
// compare states yourself before invoking the parser for clearer UX
const stateFrom = (raw) => new URLSearchParams(String(raw).split('?')[1] || '').get('state');
const s = stateFrom(callbackUrl);
if (expectedState && s && s !== expectedState) throw new Error('Stale callback — restart the connect flow (state mismatch)'); Type guard
const stateMatches = (raw, expected) => {
const s = new URLSearchParams(String(raw).split('?')[1] || '').get('state');
return !s || !expected || s === expected;
}; Try / catch
try { ({ firebaseIdToken } = parseWindsurfCallback(raw, state)); }
catch (e) {
if (e.message === 'Windsurf callback state mismatch') {
return restartFlow('Callback from a previous attempt detected — starting a fresh sign-in');
}
throw e;
} Prevention
- Generate a fresh random state per flow and store it until the callback arrives
- Run only one OAuth flow at a time per callback port/listener
- Never paste callback URLs from a previous session
- Discard in-flight flows when the user re-clicks connect
When it happens
Trigger: Two OAuth flows running concurrently (second callback delivered to the first's listener); user completing a stale auth URL from an earlier attempt; replaying an old callback URL; paste-token flow where state from a different session is pasted.
Common situations: Clicking 'connect' twice and finishing the older browser tab; re-running the flow and pasting a URL saved from the previous attempt; multiple accounts connected in parallel tabs.
Related errors
- This request isn't valid. Please restart the Kimchi login fl
- Invalid state parameter
- `Windsurf ${path} HTTP ${res.status}: ${text.slice(0, 200)}`
- `Windsurf ${path} invalid JSON`
- desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/bc11a870222b80ea.
Report an issue: GitHub.