{"record":{"id":"2417ea8ba952e31a","repo":"decolua/9router","slug":"failed-to-save-tokens-2417ea","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/gemini.js","lineNumber":149,"sourceCode":"      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        email: userInfo.email,\n        projectId: projectId,\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 Gemini OAuth flow\n   */\n  async connect() {\n    const spinner = createSpinner(\"Starting Gemini OAuth...\").start();\n\n    try {\n      spinner.text = \"Starting local server...\";\n\n      // Start local server for callback\n      let callbackParams = null;\n      const { port, close } = await startLocalServer((params) => {\n        callbackParams = params;","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/gemini.js#L131-L167","documentation":"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.","triggerScenarios":"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.","commonSituations":"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`.","solutions":["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).","Re-login to the CLI or regenerate the session token so the `Authorization: Bearer <token>` and `X-User-Id` headers are valid.","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.","If the server returns non-JSON on error, fix/patch saveTokens to use response.text() before parsing so the real error surfaces."],"exampleFix":"// before\nif (!response.ok) {\n  const error = await response.json();\n  throw new Error(error.error || \"Failed to save tokens\");\n}\n// after\nif (!response.ok) {\n  const text = await response.text();\n  let msg;\n  try { msg = JSON.parse(text).error; } catch { msg = text; }\n  throw new Error(msg || `Failed to save tokens (HTTP ${response.status})`);\n}","handlingStrategy":"validation","validationCode":"const { server, token, userId } = getServerCredentials();\nif (!server || !token) throw new Error('CLI not logged in: missing server URL or session token');\nconst health = await fetch(`${server}/api/health`).catch(() => null);\nif (!health || !health.ok) throw new Error(`9Router server unreachable at ${server}`);","typeGuard":"function isTokenSaveErrorBody(body) {\n  return body !== null && typeof body === 'object' && typeof body.error === 'string' && body.error.length > 0;\n}","tryCatchPattern":"try {\n  await service.saveTokens(tokens, userInfo, projectId);\n} catch (err) {\n  if (err.message === 'Failed to save tokens') {\n    console.error('Token upload failed — is the 9Router server running and are you logged in? Run the login flow and retry.');\n  }\n  throw err;\n}","preventionTips":["Run the login/health-check step before attempting any provider OAuth connect so server URL and session token are known-good.","Log response.status for non-OK server calls; the current message drops the HTTP status.","Use response.text() before JSON.parse so non-JSON error bodies (HTML 404/502) surface verbatim.","Wrap saveTokens in a retry with backoff for transient 5xx from the local server."],"tags":["network","http","oauth","token-storage"],"backgroundTag":"http-request-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}