tinyhumansai/openhuman · error · Error
OpenRouter OAuth callback state did not match the request.
Error message
OpenRouter OAuth callback state did not match the request.
What it means
OAuth CSRF protection: the state query parameter in the callback must equal the random state sent with the authorize request. A mismatch means the callback belongs to a different flow instance — a stale tab, a second concurrent attempt, or an injected/forged redirect.
Source
Thrown at app/src/utils/openrouterOAuth.ts:55
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
async function createCodeChallenge(verifier: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
return base64UrlEncode(new Uint8Array(digest));
}
function extractOAuthCode(callbackUrl: string, expectedState: string): string {
let parsed: URL;
try {
parsed = new URL(callbackUrl);
} catch {
throw new Error('OpenRouter OAuth returned an invalid callback URL.');
}
const actualState = parsed.searchParams.get('state');
if (actualState !== expectedState) {
throw new Error('OpenRouter OAuth callback state did not match the request.');
}
const code = parsed.searchParams.get('code');
if (!code) {
throw new Error('OpenRouter OAuth did not return an authorization code.');
}
return code;
}
async function exchangeCodeForKey(
code: string,
verifier: string,
fetchImpl: typeof fetch
): Promise<string> {
const response = await fetchImpl(OPENROUTER_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: PKCE_METHOD }),View on GitHub (pinned to a221052e0d)
Solutions
- Close prior authorize tabs before retrying the flow
- Serialize attempts — disable the OAuth trigger while a flow is in flight
- On mismatch, ignore that callback and keep listening for the one carrying the current state, with an overall timeout
Example fix
// before — first captured request decides
const url = await listener.next();
const code = extractOAuthCode(url, state);
// after — skip stale callbacks, wait for the matching one
for await (const url of listener) {
const u = new URL(url);
if (u.searchParams.get('state') !== state) continue;
var code = extractOAuthCode(url, state);
break;
} Defensive patterns
Strategy: try-catch
Validate before calling
const params = new URL(callbackUrl).searchParams;
// state presence can be checked; equality must still be enforced by extractOAuthCode
if (!params.has('state')) continue; Try / catch
Catch the state mismatch, do NOT fail the flow — keep listening for a callback whose state equals the current attempt's, bounded by an overall timeout; cancel cleanly if none arrives.
Prevention
- Allow only one OAuth flow in flight — gate the trigger UI
- Generate state per attempt and discard callbacks after the flow is cancelled
- Tell users to close stale authorize tabs before retrying
When it happens
Trigger: The user completes authorization in a browser tab from an earlier attempt while a new listener with a fresh state is running; two OAuth flows triggered concurrently; a deep-link callback replayed manually.
Common situations: Duplicate tabs left open after a failed attempt; retrying OAuth while the old tab still redirects; listener restarted with a new state while the browser holds the old authorize URL.
Related errors
- OpenRouter OAuth returned an invalid callback URL.
- OpenRouter OAuth did not return an authorization code.
- OpenRouter key exchange failed (${response.status}).
- OpenRouter key exchange succeeded but no API key was returne
- OpenRouter OAuth listener returned an invalid redirect URL.
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/bb9e85dc80412922.
Report an issue: GitHub.