Stirling-Tools/Stirling-PDF · error · Error

Failed to save authentication token

Error message

Failed to save authentication token

What it means

Thrown by AuthService.login after the Rust 'login' invoke command already returned a valid token+refresh_token, but saveTokenEverywhere() failed to persist the token into the Tauri secure store / localStorage. The login was successful server-side; only local persistence failed. The underlying storage-layer error is preserved as error.cause, and auth status is NOT changed by this throw (the outer catch will reset it to unauthenticated).

Solutions

  1. Inspect error.cause for the real storage-layer message (Tauri store write vs keyring access) and fix that specific layer.
  2. On Linux, ensure a secret-service implementation (gnome-keyring / kwallet) is running and unlocked before launching the app.
  3. Clear a corrupted store: remove the Tauri store file and localStorage key 'stirling_jwt', then retry login.
  4. If the keyring is permanently unavailable, reinstall the desktop app so the Tauri store/keyring plugins re-register.
  5. Free disk space on the volume holding the OS temp / app-data directory.

Example fix

// before: caller sees only the generic message
try { await authService.login(...); }
catch (e) { showError(e.message); }

// after: surface the real storage cause and offer retry
try { await authService.login(...); }
catch (e) {
  if (e instanceof Error && e.message === 'Failed to save authentication token') {
    showError(`Could not store your login: ${e.cause instanceof Error ? e.cause.message : e.cause}`);
    offerRetry();
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Type guard

function isTokenSaveFailure(e: unknown): e is Error & { cause: unknown } {
  return e instanceof Error && e.message === 'Failed to save authentication token';
}

Try / catch

try { await authService.login(server, user, pass); }
catch (e) {
  if (isTokenSaveFailure(e)) {
    // login worked server-side; only local persistence failed -> safe to retry login
    const cause = e instanceof Error ? (e as any).cause : undefined;
    reportStorageError(cause);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: saveTokenEverywhere(token, refreshToken) rejects. Concretely: the Tauri store plugin command fails to write, the OS keyring/credential store is locked or missing, the store JSON is corrupted, the disk is full, or a keychain permission prompt was declined.

Common situations: Running on a headless/CI Linux box with no secret-service daemon; a corporate machine where the login keychain is locked at the moment of login; a corrupted stirling_jwt / Tauri store file left from an older app version; disk-full on the volume holding the Tauri app data dir.

Understand the failure class

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/dfada1905d3dce1e. Report an issue: GitHub.

Appendix: source

Thrown at frontend/editor/src/desktop/services/authService.ts:365

        supabaseKey: SUPABASE_KEY,
        saasServerUrl: STIRLING_SAAS_URL,
      });

      const {
        token,
        username: returnedUsername,
        email,
        refresh_token: refreshToken,
      } = response;

      // Save token to all storage locations. Supabase (SaaS) logins include a
      // refresh token so the short-lived access token can be renewed; self-hosted
      // logins return null here and refresh via the current access token instead.
      try {
        await this.saveTokenEverywhere(token, refreshToken);
      } catch (error) {
        console.error("[Desktop AuthService] Failed to save token:", error);
        throw new Error("Failed to save authentication token", {
          cause: error,
        });
      }

      // Save user info to store
      await invoke("save_user_info", {
        username: returnedUsername || username,
        email,
      });

      const userInfo: UserInfo = {
        username: returnedUsername || username,
        email: email || undefined,
      };

      this.setAuthStatus("authenticated", userInfo);

      return userInfo;

View on GitHub (pinned to 9ef20dcab8)