tinyhumansai/openhuman · error

OpenRouter OAuth returned an invalid callback URL.

Error message

OpenRouter OAuth returned an invalid callback URL.

What it means

extractOAuthCode() wraps the OAuth callback string in new URL(callbackUrl); the constructor throws on anything that is not an absolute, parseable URL (no scheme/host), and this catch converts that into a domain error. The callback should be the loopback redirect (http://localhost:3000/...) or the OS deep-link URL string.

Solutions

  1. Log the raw callback string at the boundary and ensure it is the full absolute URL including scheme
  2. If you receive only a query string, prepend the loopback origin before calling extractOAuthCode
  3. Trim whitespace from OS-provided URL payloads (Windows deep links sometimes include trailing NULs/newlines)

Example fix

// before
const code = extractOAuthCode(rawFromOs, state);
// after
const raw = rawFromOs.trim();
const callbackUrl = raw.startsWith('http') ? raw : `http://localhost:${OPENROUTER_LOOPBACK_PORT}/${raw.replace(/^\/?/, '')}`;
const code = extractOAuthCode(callbackUrl, state);
Defensive patterns

Strategy: validation

Validate before calling

function isAbsoluteHttpUrl(s: string): boolean {
  try { const u = new URL(s.trim()); return u.protocol === 'http:' || u.protocol === 'https:'; }
  catch { return false; }
}
// gate before extraction:
if (!isAbsoluteHttpUrl(callbackUrl)) throw new Error('callback was not an absolute URL');

Type guard

function isParsableUrl(s: string): s is string {
  try { new URL(s); return true; } catch { return false; }
}

Try / catch

try { const code = extractOAuthCode(callbackUrl, state); }
catch (err) {
  if (err instanceof Error && err.message.includes('invalid callback URL')) {
    // log raw payload; fix the listener/deep-link shape before retrying
  } else throw err;
}

Prevention

When it happens

Trigger: The listener/deep-link layer delivering a relative or truncated string ('/callback?code=...', empty string, just the query '?code=...'); a custom-scheme URL on a runtime where URL rejects the scheme; whitespace/newline contamination of the URL.

Common situations: Loopback listener on port 3000 returning only the search part; Tauri deep-link event payload shaped differently than expected across OSes; tests passing path-only fixtures.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/10449c37bb07681b. Report an issue: GitHub.

Appendix: source

Thrown at app/src/utils/openrouterOAuth.ts:50

function base64UrlEncode(bytes: Uint8Array): string {
  let binary = '';
  for (const value of bytes) {
    binary += String.fromCharCode(value);
  }
  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

View on GitHub (pinned to 7491200858)