decolua/9router · error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

OpenAI-specific override of the generic token exchange: exchangeOpenAICode() POSTs the authorization code, client_id, redirect_uri, and code_verifier to OPENAI_CONFIG.tokenUrl, and throws when the response is not ok. The raw response body (typically `invalid_grant` or `invalid_client` JSON) is included in the message. Raised on the path exchangeOpenAICode <- tokens/connect flow.

Source

Thrown at src/lib/oauth/services/openai.js:54

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

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

    return await response.json();
  }

  /**
   * Save OpenAI tokens to server
   */
  async saveTokens(tokens) {
    const { server, token, userId } = getServerCredentials();

    const response = await fetch(`${server}/api/cli/providers/openai`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
        "X-User-Id": userId,
      },

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Run the full connect() flow again to get a fresh code and exchange it immediately; the code is single-use and short-lived.
  2. Verify the embedded body text for the exact OAuth error and address it (invalid_grant -> fresh code; invalid_client -> client_id).
  3. Ensure redirect_uri is the exact same localhost URL used in the authorize step (same port) — don't restart the CLI between authorize and exchange.
  4. Check outbound HTTPS access to OpenAI's token endpoint (proxy/VPN can return an HTML block page).
  5. Update the CLI — a stale bundled OPENAI_CONFIG.clientId can be rejected by OpenAI.

Example fix

// before
const tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier);
// after (fresh flow per attempt, immediate exchange)
const { code, codeVerifier, redirectUri } = await this.authenticate("OpenAI", this.buildOpenAIAuthUrl.bind(this));
const tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier); // exchange right away, no restart in between
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-exchange checks:
if (!code) throw new Error("No authorization code to exchange");
if (!redirectUri.startsWith("http://localhost:")) throw new Error("redirect_uri must match the authorize-time localhost URL");

Type guard

function isOpenAITokenResponse(json) {
  return json != null && typeof json === "object" && typeof json.access_token === "string";
}

Try / catch

try {
  const tokens = await service.exchangeOpenAICode(code, redirectUri, codeVerifier);
} catch (err) {
  if (err.message.startsWith("Token exchange failed:")) {
    if (err.message.includes("invalid_grant")) { /* restart full flow for a fresh code */ }
    else if (err.message.includes("invalid_client")) { /* update CLI / check OPENAI_CONFIG.clientId */ }
    else throw err;
  } else throw err;
}

Prevention

When it happens

Trigger: connect() -> authenticate() succeeds, then exchangeOpenAICode(code, redirectUri, codeVerifier) gets !response.ok from OpenAI's token endpoint — the one-time code expired or was redeemed, code_verifier doesn't match the challenge, redirect_uri (the ephemeral localhost:port/callback) differs from the authorize request, or the registered OpenAI client_id is rejected.

Common situations: Waiting too long between browser approval and exchange (code TTL); re-running after a partial failure reuses the old code; a different port bound on restart changes redirect_uri; OpenAI rotating/invalidating the client_id packaged with the CLI; network egress blocked to auth.openai.com.

Related errors


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