decolua/9router · error · Error

Failed to save tokens

Error message

Failed to save tokens

What it means

GeminiCLIService.saveTokens() POSTs the OAuth tokens, user email and projectId to the local 9Router server at `/api/cli/providers/gemini-cli`. When that HTTP response is not OK, it parses the JSON body and re-throws the server's `error` field; if the body has no `error` field (or the body is not valid JSON), the generic fallback message "Failed to save tokens" is thrown. So this error means the token upload to the local gateway server failed, and the server did not report a specific reason.

Source

Thrown at src/lib/oauth/services/gemini.js:149

      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,
      }),
    });

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

    return await response.json();
  }

  /**
   * Complete Gemini OAuth flow
   */
  async connect() {
    const spinner = createSpinner("Starting Gemini 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. Verify the 9Router server is running and that the `server` URL from getServerCredentials() (env/config) matches its actual host and port (default http://localhost:20128).
  2. Re-login to the CLI or regenerate the session token so the `Authorization: Bearer <token>` and `X-User-Id` headers are valid.
  3. Check the server logs for the /api/cli/providers/gemini-cli route to see the real status code, since this message hides the HTTP status.
  4. If the server returns non-JSON on error, fix/patch saveTokens to use response.text() before parsing so the real error surfaces.

Example fix

// before
if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error || "Failed to save tokens");
}
// after
if (!response.ok) {
  const text = await response.text();
  let msg;
  try { msg = JSON.parse(text).error; } catch { msg = text; }
  throw new Error(msg || `Failed to save tokens (HTTP ${response.status})`);
}
Defensive patterns

Strategy: validation

Validate before calling

const { server, token, userId } = getServerCredentials();
if (!server || !token) throw new Error('CLI not logged in: missing server URL or session token');
const health = await fetch(`${server}/api/health`).catch(() => null);
if (!health || !health.ok) throw new Error(`9Router server unreachable at ${server}`);

Type guard

function isTokenSaveErrorBody(body) {
  return body !== null && typeof body === 'object' && typeof body.error === 'string' && body.error.length > 0;
}

Try / catch

try {
  await service.saveTokens(tokens, userInfo, projectId);
} catch (err) {
  if (err.message === 'Failed to save tokens') {
    console.error('Token upload failed — is the 9Router server running and are you logged in? Run the login flow and retry.');
  }
  throw err;
}

Prevention

When it happens

Trigger: saveTokens(tokens, userInfo, projectId) called at the end of GeminiCLIService.connect() when fetch() returns a non-2xx status from `${server}/api/cli/providers/gemini-cli` and response.json() either has no `error` property or fails to parse.

Common situations: The local server is not running or `server` from getServerCredentials() points at the wrong host/port; the CLI session token (`token`) is stale so the server returns 401 without an `error` field; the server route returns an HTML error page (e.g. 404/502 from a proxy) instead of JSON, making response.json() throw or return an object without `error`.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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