decolua/9router · error · Error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

AntigravityService.exchangeCode POSTs grant_type=authorization_code (with client_secret) to the configured tokenUrl and throws this on any non-2xx response, embedding the raw body. It is the standard OAuth2 token-endpoint failure: the authorization code could not be exchanged for tokens.

Source

Thrown at src/lib/oauth/services/antigravity.js:55

  async exchangeCode(code, redirectUri) {
    const response = await fetch(this.config.tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret,
        code: code,
        redirect_uri: redirectUri,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    return await response.json();
  }

  /**
   * Get user info from Google
   */
  async getUserInfo(accessToken) {
    const response = await fetch(`${this.config.userInfoUrl}?alt=json`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Accept: "application/json",
      },
    });

    if (!response.ok) {
      const error = await response.text();

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the embedded OAuth error body (invalid_grant, invalid_client, redirect_uri_mismatch) and act on it
  2. Restart the full OAuth flow with a fresh code — never retry a used code
  3. Verify client_id/client_secret and redirect_uri in ANTIGRAVITY_CONFIG exactly match the registered OAuth client
  4. Confirm the callback redirect_uri string is byte-identical to the one in buildAuthUrl
  5. If 5xx, wait and retry with a brand-new authorization code

Example fix

// before
const tokens = await svc.exchangeCode(code, redirectUri);
// after
let tokens;
try { tokens = await svc.exchangeCode(code, redirectUri); }
catch (e) {
  if (/invalid_grant/.test(e.message)) throw new Error('Code expired or already used — reconnect to restart the flow');
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight before exchanging
if (!code) throw new Error('No authorization code — restart OAuth');
if (!ANTIGRAVITY_CONFIG.clientId || !ANTIGRAVITY_CONFIG.clientSecret) throw new Error('Antigravity OAuth client credentials missing');
if (authRedirectUri !== redirectUri) throw new Error('redirect_uri differs from the authorize request');

Type guard

const isExchangeError = (e) => e instanceof Error && e.message.startsWith('Token exchange failed:');
const oauthErrorCode = (e) => { const m = e.message.match(/"error"\s*:\s*"([^"]+)"/); return m ? m[1] : null; };

Try / catch

try { tokens = await svc.exchangeCode(code, redirectUri); }
catch (e) {
  if (!isExchangeError(e)) throw e;
  const codeErr = oauthErrorCode(e);           // invalid_grant | invalid_client | redirect_uri_mismatch
  if (codeErr === 'invalid_grant') startNewOAuthFlow();
  else throw new Error(`Antigravity client config problem (${codeErr}) — check clientId/secret/redirect_uri`);
}

Prevention

When it happens

Trigger: Code expired/already used (invalid_grant); client_id/client_secret wrong or rotated; redirect_uri mismatch vs the authorize request; Google token endpoint returning 400/401/5xx; clock skew affecting code validity.

Common situations: Retrying after a failed first exchange (code consumed); ANTIGRAVITY_CONFIG credentials outdated; callback served on a different port/path than registered; Google-side outage.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/a685b605d2f047b9. Report an issue: GitHub.