{"record":{"id":"c34470662f6bd288","repo":"decolua/9router","slug":"failed-to-save-tokens-c34470","errorCode":null,"errorMessage":"Failed to save tokens","messagePattern":"Failed to save tokens","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/iflow.js","lineNumber":116,"sourceCode":"    const response = await fetch(`${server}/api/cli/providers/iflow`, {\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        apiKey: userInfo.apiKey,\n        email: userInfo.email || userInfo.phone,\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 iFlow OAuth flow\n   */\n  async connect() {\n    const spinner = createSpinner(\"Starting iFlow 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":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/iflow.js#L98-L134","documentation":"Thrown by IFlowService.saveTokens() when the POST of the exchanged tokens and user profile to the 9Router server endpoint `${server}/api/cli/providers/iflow` returns non-OK. The CLI reads the server's JSON `error` field; this fallback fires when the server gave no specific error. GitHub auth has succeeded at this point — the failure is purely in persisting the connection server-side.","triggerScenarios":"saveTokens(tokens, userInfo) hits a server that returns 401 (expired Bearer token / wrong X-User-Id), 404 (route missing — CLI/server version mismatch), 400 (validation rejected e.g. missing apiKey), or 500 with a body lacking `error`.","commonSituations":"Stale JWT after server restart/JWT_SECRET rotation; wrong server URL in config; dashboard version older than the CLI expecting the iflow provider route; server DB failure returning an empty error body.","solutions":["Verify the server is reachable (`curl ${server}/api/health`) and the URL config is correct.","Re-login to the dashboard to refresh the Bearer token used by getServerCredentials().","Ensure CLI and dashboard versions match so /api/cli/providers/iflow exists.","Check server logs for the concrete rejection (validation, DB, auth).","Retry — transient 5xx may pass on a second attempt."],"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 body = await response.text();\n  let msg; try { msg = JSON.parse(body).error; } catch {}\n  throw new Error(msg || `Failed to save tokens (HTTP ${response.status}): ${body.slice(0, 200)}`);\n}","handlingStrategy":"try-catch","validationCode":"// Validate server credentials and required payload before the upload\nconst { server, token, userId } = getServerCredentials();\nif (!server || !token) throw new Error(\"Missing server credentials — log in to the dashboard first\");\nif (!tokens.access_token || !userInfo.apiKey) throw new Error(\"Missing accessToken/apiKey required by the iflow provider endpoint\");","typeGuard":"function hasServerError(data) {\n  return data !== null && typeof data === \"object\" && typeof data.error === \"string\";\n}","tryCatchPattern":"try {\n  await iflowService.saveTokens(tokens, userInfo);\n} catch (err) {\n  if (/Failed to save tokens/.test(err.message) || err.message === \"Failed to save tokens\") {\n    console.error(\"Server rejected the iflow connection — check server reachability, JWT freshness, and server logs.\", err.message);\n  } else { throw err; }\n}","preventionTips":["Confirm server/URL/token config before starting the OAuth flow.","Refresh the dashboard JWT when expired — 401s surface as this error.","Match CLI and dashboard versions so the /api/cli/providers/iflow route exists.","Include all required payload fields (accessToken, refreshToken, apiKey, email) to avoid 400s."],"tags":["http","server","oauth","persistence"],"backgroundTag":"http-request-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}