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 in

View on GitHub (pinned to a221052e0d)

Solutions

  1. Restart the whole flow (new verifier + new authorize request) — a single-use code cannot be re-exchanged
  2. Verify the code_verifier is the exact string hashed for the challenge
  3. On 5xx, back off and retry later; check OpenRouter status
  4. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/e2c3a604447b8d63. Report an issue: GitHub.