decolua/9router · error · Error

Failed to save tokens

Error message

Failed to save tokens

What it means

Thrown by AntigravityService.saveTokens when the POST to the local 9router server (`${server}/api/cli/providers/antigravity`) returns a non-2xx status. The server's JSON `error` field is preferred in the message, with the generic string as fallback. The OAuth flow itself succeeded — only persisting credentials into the dashboard failed, so no provider account gets registered.

Source

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

      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,
        scope: tokens.scope,
        email: userInfo.email,
        projectId: projectId, // Send projectId to server
      }),
    });

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

    return await response.json();
  }

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

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

      // Start local server for callback
      let callbackParams = null;
      const { port, close } = await startLocalServer((params) => {
        callbackParams = params;

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the server-provided `error` message in the thrown Error — it names the exact rejection reason
  2. Re-authenticate the CLI with the server (re-login) so the Bearer token in getServerCredentials() is fresh
  3. Verify the `server` URL in your CLI config points at the running 9router dashboard instance
  4. Retry after confirming the dashboard is up — the Google tokens may still be in your terminal output and only the save step needs repeating
Defensive patterns

Strategy: try-catch

Validate before calling

const { server, token, userId } = getServerCredentials();
if (!server || !token) throw new Error('CLI not authenticated with a 9router server — run login first');
const ping = await fetch(`${server}/api/health`).catch(() => null);
if (!ping || !ping.ok) throw new Error(`Dashboard at ${server} is not reachable`);

Type guard

function hasServerCreds(c) { return c && typeof c.server === 'string' && c.server.startsWith('http') && typeof c.token === 'string' && c.token.length > 0; }

Try / catch

try {
  await service.saveTokens(tokens, userInfo, projectId);
} catch (e) {
  if (e.message === 'Failed to save tokens' || /save tokens/i.test(e.message)) {
    // re-login CLI, verify server URL, then retry save — Google tokens are still valid
  } else throw e;
}

Prevention

When it happens

Trigger: The dashboard API rejects the save: expired/invalid CLI Bearer token, wrong `server` URL from getServerCredentials(), missing X-User-Id, duplicate account rejected server-side, or the server is unreachable/restarting.

Common situations: CLI connected to a different/older 9router server than the one that issued the CLI token; JWT session expired after long OAuth round-trip; server restarted during the Google login; misconfigured server URL in CLI config.

Related errors


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