{"record":{"id":"10449c37bb07681b","repo":"tinyhumansai/openhuman","slug":"openrouter-oauth-returned-an-invalid-callback-url","errorCode":null,"errorMessage":"OpenRouter OAuth returned an invalid callback URL.","messagePattern":"OpenRouter OAuth returned an invalid callback URL\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/utils/openrouterOAuth.ts","lineNumber":50,"sourceCode":"function base64UrlEncode(bytes: Uint8Array): string {\n  let binary = '';\n  for (const value of bytes) {\n    binary += String.fromCharCode(value);\n  }\n  return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\nasync function createCodeChallenge(verifier: string): Promise<string> {\n  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n  return base64UrlEncode(new Uint8Array(digest));\n}\n\nfunction extractOAuthCode(callbackUrl: string, expectedState: string): string {\n  let parsed: URL;\n  try {\n    parsed = new URL(callbackUrl);\n  } catch {\n    throw new Error('OpenRouter OAuth returned an invalid callback URL.');\n  }\n\n  const actualState = parsed.searchParams.get('state');\n  if (actualState !== expectedState) {\n    throw new Error('OpenRouter OAuth callback state did not match the request.');\n  }\n\n  const code = parsed.searchParams.get('code');\n  if (!code) {\n    throw new Error('OpenRouter OAuth did not return an authorization code.');\n  }\n  return code;\n}\n\nasync function exchangeCodeForKey(\n  code: string,\n  verifier: string,\n  fetchImpl: typeof fetch","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/utils/openrouterOAuth.ts#L32-L68","documentation":"extractOAuthCode runs new URL(callbackUrl) inside try/catch; anything the WHATWG URL constructor cannot parse (empty string, relative path, fragment-only garbage captured from the loopback listener) throws and is rethrown as this message.","triggerScenarios":"The loopback OAuth listener hands back an empty or malformed capture — the socket closed before the request line was read, or the first non-OAuth request (favicon, probe) was mistaken for the callback.","commonSituations":"Browser firing an extra request first; listener race where the connection reset mid-read; port-fallback logic returning an incomplete string.","solutions":["Log the raw callbackUrl — it is usually empty or a fragment","Make the listener skip requests whose path does not match the expected callback path","If empty captures recur, keep listening for the next request instead of failing on the first"],"exampleFix":"// before — first captured request decides\nconst url = await listener.next();\nconst code = extractOAuthCode(url, state);\n\n// after — only requests that parse and match the path count\nfor await (const url of listener) {\n  if (!url.startsWith('/callback')) continue;\n  const code = extractOAuthCode(url, state);\n  break;\n}","handlingStrategy":"validation","validationCode":"function isParsableUrl(u: string): boolean {\n  try {\n    new URL(u);\n    return true;\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"Catch the parse failure, keep the listener open, and wait for the next captured request — the first request is often a favicon or probe, not the OAuth callback.","preventionTips":["Filter loopback requests by path before treating any as the callback","Never assume the first captured request is the redirect","Propagate listener socket errors instead of coalescing them to empty strings"],"tags":["oauth","openrouter","url","loopback"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}