decolua/9router · error

Failed to save tokens

Error message

Failed to save tokens

What it means

Thrown by OpenAIService.saveTokens() when the POST of the freshly obtained tokens to the dashboard server (`${server}/api/cli/providers/openai`) returns a non-2xx status. It surfaces the server's own `error` field when present, else the generic fallback message. The OAuth exchange itself succeeded; only persisting the tokens to the local server failed.

Source

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

    const response = await fetch(`${server}/api/cli/providers/openai`, {
      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,
        idToken: tokens.id_token,
        scope: tokens.scope,
      }),
    });

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

    return await response.json();
  }

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

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

      // Authenticate and get authorization code
      const { code, codeVerifier, redirectUri } = await this.authenticate(
        "OpenAI",
        this.buildOpenAIAuthUrl.bind(this)

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the thrown `error.error` value — it names the server-side reason (401 auth vs 404 route vs 400 validation).
  2. Re-login to refresh the Bearer token / credentials used by getServerCredentials(); expired sessions are the top cause.
  3. Confirm the dashboard server is running and that `server` in getServerCredentials() points to the correct host:port.
  4. Verify server and CLI versions match so the /api/cli/providers/openai endpoint exists and accepts the payload.
  5. Check server logs for the failing request to see the underlying persistence error.

Example fix

// before
if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error || "Failed to save tokens");
}
// after (keep status, survive non-JSON bodies)
if (!response.ok) {
  let msg = `Failed to save tokens (HTTP ${response.status})`;
  try { const e = await response.json(); if (e && e.error) msg = `${msg}: ${e.error}`; } catch {}
  throw new Error(msg);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before saveTokens, verify server credentials are present and reachable:
const { server, token, userId } = getServerCredentials();
if (!server || !token) throw new Error("Not logged in: missing server credentials");
await fetch(`${server}/api/health`).catch(() => { throw new Error(`Server unreachable at ${server}`); });

Type guard

function isSaveResponse(json) {
  return json != null && typeof json === "object" && !json.error;
}

Try / catch

try {
  await service.saveTokens(tokens);
} catch (err) {
  if (err.message === "Failed to save tokens" || /Failed to save tokens/.test(err.message)) {
    // re-authenticate CLI session, confirm dashboard server is running, then retry
  } else throw err;
}

Prevention

When it happens

Trigger: connect() -> saveTokens(tokens) posts to the server with Bearer token and X-User-Id headers from getServerCredentials(); the server replies !response.ok with a JSON body — auth rejected (expired/invalid JWT or wrong user), server not running at the configured URL, the OpenAI provider endpoint missing, or a validation rejection of the payload.

Common situations: Dashboard server not started or URL/port misconfigured (getServerCredentials pointing at the wrong host); CLI session/JWT expired; user id mismatch between CLI login and server; server version older than the endpoint the CLI calls; database write failure on the server side.

Related errors


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