{"record":{"id":"aee69a5c431423ab","repo":"actualbudget/actual","slug":"login-user-token-not-set","errorCode":null,"errorMessage":"login: User token not set","messagePattern":"login: User token not set","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/auth/app.ts","lineNumber":278,"sourceCode":"      throw new Error('No sync server configured.');\n    }\n    res = await post(serverConfig.SIGNUP_SERVER + '/login', loginInfo);\n  } catch (err) {\n    if (err instanceof PostError) {\n      return {\n        error: err.reason || 'network-failure',\n      };\n    }\n\n    throw err;\n  }\n\n  if (res.returnUrl) {\n    return { redirectUrl: res.returnUrl };\n  }\n\n  if (!res.token) {\n    throw new Error('login: User token not set');\n  }\n\n  await asyncStorage.setItem('user-token', res.token);\n  return {};\n}\n\nasync function signOut() {\n  encryption.unloadAllKeys();\n  await asyncStorage.multiRemove([\n    'user-token',\n    'encrypt-keys',\n    'lastBudget',\n    'readOnly',\n  ]);\n  return 'ok';\n}\n\nasync function setToken({ token }: { token: string }) {","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/auth/app.ts#L260-L296","documentation":"After a sign-in exchange with the sync server, signIn expects the server response to contain a session token; if res.token is missing the response shape is unexpected and it throws rather than storing an undefined token.","triggerScenarios":"Calling app.signIn with credentials when the server responds 200 without a token field — e.g. incompatible server version, an OpenID flow that did not finalize, or a proxy returning a non-standard body.","commonSituations":"Server/client version mismatch after an upgrade; reverse proxy stripping or altering the JSON body; pointing the client at a non-Actual endpoint that returns 200 OK with HTML/other JSON.","solutions":["Check that the sync server and client versions are compatible; upgrade the sync server.","Verify the endpoint is a real Actual sync server (SIGNUP_SERVER) and not a proxy masking errors.","Inspect the sign-in response for an error field (PostError reasons like 'invalid-password') and surface it before assuming success."],"exampleFix":"// before\nconst res = await send(signupServer + '/login', { username, password });\nif (res.token) ... // server version mismatch: no token, throws later\n// after\nif (res.token == null) {\n  throw new Error('Sign-in failed: ' + (res.reason || 'no token returned'));\n}","handlingStrategy":"try-catch","validationCode":"function expectsToken(res) { return res && typeof res.token === 'string' && res.token.length > 0; }\nif (!expectsToken(signInResponse)) throw new Error('Sign-in response missing token; check server version/compatibility');","typeGuard":"function hasToken(res: unknown): res is { token: string } {\n  return typeof res === 'object' && res !== null && 'token' in res && typeof (res as { token?: unknown }).token === 'string';\n}","tryCatchPattern":"try {\n  await app.signIn({ password, useOpenId });\n} catch (e) {\n  if (e.message.includes('User token not set')) {\n    console.error('Server returned no token — verify sync-server version matches the client and the endpoint is a real Actual server.');\n  } else throw e;\n}","preventionTips":["Keep sync-server and client versions in lockstep","Do not put a proxy that rewrites response bodies in front of the server","Log/surface res.reason from the sign-in endpoint before treating the call as successful"],"tags":["auth","sync-server","unexpected-response"],"backgroundTag":"missing-auth-token","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}