decolua/9router · error · Error

Failed to save tokens

Error message

Failed to save tokens

What it means

Thrown by CodexService.saveTokens when the POST to `${server}/api/cli/providers/codex` returns a non-2xx status. It prefers the server's JSON `error` field, falling back to the generic string. The OpenAI/Codex OAuth flow completed — persisting the tokens (accessToken, refreshToken, expiresIn, lastRefreshAt) into the dashboard failed, so the Codex account is not registered.

Source

Thrown at src/lib/oauth/services/codex.js:63

    const response = await fetch(`${server}/api/cli/providers/codex`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
        "X-User-Id": userId,
      },
      body: JSON.stringify({
        accessToken: tokens.access_token,
        refreshToken: tokens.refresh_token,
        expiresIn: tokens.expires_in,
        lastRefreshAt: new Date().toISOString(),
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || "Failed to save tokens");
    }

    return await response.json();
  }

  /**
   * Complete Codex OAuth flow
   */
  async connect() {
    const spinner = createSpinner("Starting Codex OAuth...").start();

    try {
      spinner.text = "Starting local server...";

      // Start local server for callback (use fixed port 1455 like real Codex CLI)
      const fixedPort = CODEX_CONFIG.fixedPort;
      let callbackParams = null;
      const { port, close } = await startLocalServer((params) => {

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the server-provided `error` message in the thrown Error for the root cause
  2. Re-login the CLI to the dashboard to refresh the Bearer token, then retry the Codex connect
  3. Verify the configured `server` URL/port matches the running 9router dashboard
  4. Retry connect after confirming the dashboard is healthy and reachable from the CLI
Defensive patterns

Strategy: try-catch

Validate before calling

const { server, token } = getServerCredentials();
if (!server || !token) throw new Error('CLI not authenticated — login to the dashboard before codex connect');
try { new URL(server); } catch { throw new Error(`Invalid server URL in CLI config: ${server}`); }

Type guard

function hasValidTarget(c) { return c && typeof c.server === 'string' && /^https?:\/\//.test(c.server) && typeof c.token === 'string' && c.token.length > 0; }

Try / catch

try {
  await service.saveTokens(tokens);
} catch (e) {
  if (/Failed to save tokens/i.test(e.message)) {
    // re-auth CLI, confirm server URL/health, then retry — Codex tokens remain valid
  } else throw e;
}

Prevention

When it happens

Trigger: Dashboard rejects the save: expired/invalid CLI Bearer token, unreachable or restarting server, wrong `server` URL from getServerCredentials(), or server-side validation rejecting the payload (e.g. missing refreshToken).

Common situations: CLI and dashboard version mismatch causing payload validation failure; CLI token expired during the browser login round-trip; firewall blocking the CLI's localhost POST; pointing the CLI at a different 9router install than the one that issued its credentials.

Related errors


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