tinyhumansai/openhuman · error · Error
OpenRouter key exchange failed (${response.status}).
Error message
OpenRouter key exchange failed (${response.status}). What it means
The PKCE code-to-key exchange POST to OPENROUTER_TOKEN_URL returned non-OK and the body carried no usable error string, so the HTTP status is surfaced as the only diagnostic. 4xx usually means a bad, expired, or already-consumed code or a verifier mismatch; 5xx is an OpenRouter-side incident.
Source
Thrown at app/src/utils/openrouterOAuth.ts:90
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: PKCE_METHOD }),
});
let body: OpenRouterExchangeResponse | null = null;
try {
body = (await response.json()) as OpenRouterExchangeResponse;
} catch {
body = null;
}
if (!response.ok) {
const detail =
typeof body?.error === 'string'
? body.error
: body?.error && typeof body.error === 'object'
? body.error.message
: null;
throw new Error(detail || `OpenRouter key exchange failed (${response.status}).`);
}
if (!body?.key || typeof body.key !== 'string') {
throw new Error('OpenRouter key exchange succeeded but no API key was returned.');
}
return body.key;
}
function toOpenRouterCallbackUrl(redirectUri: string): string {
let parsed: URL;
try {
parsed = new URL(redirectUri);
} catch {
throw new Error('OpenRouter OAuth listener returned an invalid redirect URL.');
}
// Preserve the port the loopback listener actually bound to (carried inView on GitHub (pinned to a221052e0d)
Solutions
- Restart the whole flow (new verifier + new authorize request) — a single-use code cannot be re-exchanged
- Verify the code_verifier is the exact string hashed for the challenge
- On 5xx, back off and retry later; check OpenRouter status
- Log the response body even when its shape is unexpected — it may name the reason
Example fix
// before
const key = await exchangeCodeForKey(code, verifier, fetch);
// after — 4xx restarts the flow, 5xx retries the exchange with backoff
try {
key = await exchangeCodeForKey(code, verifier, fetch);
} catch (e) {
if (/\(4\d\d\)/.test(e.message)) return startOAuthFlow();
if (/\(5\d\d\)/.test(e.message)) { await backoff(); key = await exchangeCodeForKey(code, verifier, fetch); }
else throw e;
} Defensive patterns
Strategy: retry
Try / catch
Inspect the embedded status: 4xx → abandon the exchange and restart the full OAuth flow (new verifier, new code); 5xx → retry the same exchange with exponential backoff, capped at 2-3 attempts; surface the status and any logged body otherwise.
Prevention
- Exchange the code immediately after capturing the callback — codes are short-lived and single-use
- Never re-post a consumed code across retries; restart the flow instead
- Ensure code_verifier is the exact string used to build the SHA-256 challenge
When it happens
Trigger: Exchanging a code past its short TTL or one already consumed by an earlier attempt; code_verifier differing from the SHA-256 code_challenge sent at authorize time; OpenRouter returning 5xx.
Common situations: Debugger pause or slow machine letting the code expire before the exchange; retry logic re-posting the same code after a first failure; replayed captured codes in tests.
Related errors
- OpenRouter OAuth returned an invalid callback URL.
- OpenRouter OAuth callback state did not match the request.
- OpenRouter OAuth did not return an authorization code.
- 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/e2c3a604447b8d63.
Report an issue: GitHub.