{"record":{"id":"44faeb219643fe4b","repo":"decolua/9router","slug":"failed-to-save-tokens-44faeb","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/codex.js","lineNumber":63,"sourceCode":"\n    const response = await fetch(`${server}/api/cli/providers/codex`, {\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        lastRefreshAt: new Date().toISOString(),\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 Codex OAuth flow\n   */\n  async connect() {\n    const spinner = createSpinner(\"Starting Codex OAuth...\").start();\n\n    try {\n      spinner.text = \"Starting local server...\";\n\n      // Start local server for callback (use fixed port 1455 like real Codex CLI)\n      const fixedPort = CODEX_CONFIG.fixedPort;\n      let callbackParams = null;\n      const { port, close } = await startLocalServer((params) => {","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/codex.js#L45-L81","documentation":"Thrown by CodexService.saveTokens when the POST to `${server}/api/cli/providers/codex` returns a non-2xx status. It prefers the server's JSON `error` field, falling back to the generic string. The OpenAI/Codex OAuth flow completed — persisting the tokens (accessToken, refreshToken, expiresIn, lastRefreshAt) into the dashboard failed, so the Codex account is not registered.","triggerScenarios":"Dashboard rejects the save: expired/invalid CLI Bearer token, unreachable or restarting server, wrong `server` URL from getServerCredentials(), or server-side validation rejecting the payload (e.g. missing refreshToken).","commonSituations":"CLI and dashboard version mismatch causing payload validation failure; CLI token expired during the browser login round-trip; firewall blocking the CLI's localhost POST; pointing the CLI at a different 9router install than the one that issued its credentials.","solutions":["Read the server-provided `error` message in the thrown Error for the root cause","Re-login the CLI to the dashboard to refresh the Bearer token, then retry the Codex connect","Verify the configured `server` URL/port matches the running 9router dashboard","Retry connect after confirming the dashboard is healthy and reachable from the CLI"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const { server, token } = getServerCredentials();\nif (!server || !token) throw new Error('CLI not authenticated — login to the dashboard before codex connect');\ntry { new URL(server); } catch { throw new Error(`Invalid server URL in CLI config: ${server}`); }","typeGuard":"function hasValidTarget(c) { return c && typeof c.server === 'string' && /^https?:\\/\\//.test(c.server) && typeof c.token === 'string' && c.token.length > 0; }","tryCatchPattern":"try {\n  await service.saveTokens(tokens);\n} catch (e) {\n  if (/Failed to save tokens/i.test(e.message)) {\n    // re-auth CLI, confirm server URL/health, then retry — Codex tokens remain valid\n  } else throw e;\n}","preventionTips":["Keep CLI and dashboard versions aligned so the payload schema matches","Refresh the CLI session token before starting the browser-based Codex login","Verify dashboard health before running connect","Inspect dashboard logs for /api/cli/providers/codex rejection reasons"],"tags":["oauth","persistence","http-error","codex","local-server"],"backgroundTag":"token-save-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}