{"record":{"id":"1530842394a69bce","repo":"tinyhumansai/openhuman","slug":"openrouter-key-exchange-succeeded-but-no-api-key-w","errorCode":null,"errorMessage":"OpenRouter key exchange succeeded but no API key was returned.","messagePattern":"OpenRouter key exchange succeeded but no API key was returned\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/utils/openrouterOAuth.ts","lineNumber":94,"sourceCode":"  let body: OpenRouterExchangeResponse | null = null;\n  try {\n    body = (await response.json()) as OpenRouterExchangeResponse;\n  } catch {\n    body = null;\n  }\n\n  if (!response.ok) {\n    const detail =\n      typeof body?.error === 'string'\n        ? body.error\n        : body?.error && typeof body.error === 'object'\n          ? body.error.message\n          : null;\n    throw new Error(detail || `OpenRouter key exchange failed (${response.status}).`);\n  }\n\n  if (!body?.key || typeof body.key !== 'string') {\n    throw new Error('OpenRouter key exchange succeeded but no API key was returned.');\n  }\n\n  return body.key;\n}\n\nfunction toOpenRouterCallbackUrl(redirectUri: string): string {\n  let parsed: URL;\n  try {\n    parsed = new URL(redirectUri);\n  } catch {\n    throw new Error('OpenRouter OAuth listener returned an invalid redirect URL.');\n  }\n\n  // Preserve the port the loopback listener actually bound to (carried in\n  // redirectUri): when the requested port is busy, the Tauri command falls back\n  // to an OS-assigned ephemeral port, so hardcoding OPENROUTER_LOOPBACK_PORT here\n  // sent OpenRouter a callback_url pointing at the wrong port. The PKCE\n  // callback_url is per-request, so the dynamic port is valid (this matches the","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/utils/openrouterOAuth.ts#L76-L112","documentation":"OpenRouter answered the key exchange with 2xx but the JSON body lacks a string `key` field. This is a response-contract mismatch — the parser no longer matches what the API returns (or a proxy rewrote the body); retrying the same exchange rarely helps.","triggerScenarios":"The token endpoint succeeds but returns {} , a non-string key, or a wrapped envelope; API version drift after an upstream OpenRouter change; a rewriting proxy mangling the JSON body.","commonSituations":"OpenRouter ships a response-shape change; corporate proxy strips or wraps JSON; a test mock returning the wrong fixture.","solutions":["Log the raw body's keys to confirm the shape drifted","Update the OpenRouterExchangeResponse parsing to the current API shape","If a proxy rewrites bodies, bypass it for the token URL","Pin expectations with a recorded-fixture contract test so drift is caught before users"],"exampleFix":"// before\nreturn body.key;\n\n// after — fail loudly with the observed shape\nif (!body?.key || typeof body.key !== 'string') {\n  throw new Error(`OpenRouter response shape changed; keys seen: ${Object.keys(body ?? {}).join(',')}`);\n}\nreturn body.key;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isKeyResponse(b: unknown): b is { key: string } {\n  return (\n    !!b &&\n    typeof b === 'object' &&\n    typeof (b as { key?: unknown }).key === 'string' &&\n    (b as { key: string }).key.length > 0\n  );\n}","tryCatchPattern":"Catch, log the observed body keys, and surface an explicit 'OpenRouter response format changed' error — do not store an empty key or silently succeed; retrying the same exchange will not fix a contract mismatch.","preventionTips":["Pin the token-endpoint response shape with a recorded-fixture contract test","Fail loudly on shape drift instead of defaulting to an empty key","Keep corporate proxies off the token endpoint so bodies are not rewritten"],"tags":["oauth","openrouter","api-contract","json"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}