{"record":{"id":"12d5bdda3d5633e0","repo":"decolua/9router","slug":"failed-to-save-tokens-12d5bd","errorCode":null,"errorMessage":"Failed to save tokens","messagePattern":"Failed to save tokens","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/claude.js","lineNumber":97,"sourceCode":"    // Server will auto-generate displayName based on existing account count\n    const response = await fetch(`${server}/api/cli/providers/claude`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${token}`,\n        \"X-User-Id\": userId,\n      },\n      body: JSON.stringify({\n        accessToken: tokens.access_token,\n        refreshToken: tokens.refresh_token,\n        expiresIn: tokens.expires_in,\n        scope: tokens.scope,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.error || \"Failed to save tokens\");\n    }\n\n    return await response.json();\n  }\n\n  /**\n   * Complete Claude OAuth flow\n   */\n  async connect() {\n    const spinner = createSpinner(\"Starting Claude OAuth...\").start();\n\n    try {\n      spinner.text = \"Starting local server...\";\n\n      // Authenticate and get authorization code\n      const { code, state, codeVerifier, redirectUri } = await this.authenticate(\n        \"Claude\",\n        this.buildClaudeAuthUrl.bind(this)","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/claude.js#L79-L115","documentation":"Thrown by ClaudeService.saveTokens when the POST to the 9router dashboard endpoint `${server}/api/cli/providers/claude` returns non-2xx. The server's JSON `error` field is used when present, otherwise the generic fallback string. The Claude OAuth exchange already succeeded — only registering the tokens with the local server failed, so the account is not usable in the router.","triggerScenarios":"Dashboard API rejects the save: invalid/expired CLI Bearer token from getServerCredentials(), wrong `server` URL, server restarted mid-flow, validation rejection of missing/empty token fields, or duplicate-account constraints.","commonSituations":"CLI pointed at a stale server URL in its config; session token expired during the browser-based Claude login; dashboard updated/restarted between authorize and save; running CLI against a different 9router instance than the one that issued credentials.","solutions":["Inspect the server's `error` message inside the thrown Error for the exact rejection","Re-authenticate the CLI with the dashboard to refresh the Bearer token, then retry connect","Confirm the `server` URL in CLI config matches the running 9router instance and port","Retry the connect flow once the dashboard is confirmed up and reachable"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const { server, token, userId } = getServerCredentials();\nif (!server || !token) throw new Error('CLI not authenticated — run login before connecting Claude');\nconst health = await fetch(`${server}/api/health`).catch(() => null);\nif (!health || !health.ok) throw new Error(`Dashboard ${server} unreachable`);","typeGuard":"function canSave(c) { return c && typeof c.server === 'string' && /^https?:\\/\\//.test(c.server) && typeof c.token === 'string' && c.token.length > 0 && typeof c.userId !== 'undefined'; }","tryCatchPattern":"try {\n  await service.saveTokens(tokens);\n} catch (e) {\n  if (/Failed to save tokens/i.test(e.message)) {\n    // server reachable but rejected: check server error text, refresh CLI auth, retry\n  } else throw e;\n}","preventionTips":["Refresh CLI login when the session token nears expiry before long OAuth flows","Confirm the dashboard is running and the server URL/port in CLI config is correct","Avoid restarting the dashboard between authorize and save","Check dashboard logs for /api/cli/providers/claude rejection details"],"tags":["oauth","persistence","http-error","claude","local-server"],"backgroundTag":"token-save-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}