{"record":{"id":"4aef2ff5ae4c0120","repo":"actualbudget/actual","slug":"token-not-found","errorCode":"token-not-found","errorMessage":"token-not-found","messagePattern":"token-not-found","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"packages/sync-server/src/util/validate-user.ts","lineNumber":20,"sourceCode":"import ipaddr from 'ipaddr.js';\n\nimport { getSession } from '#account-db';\nimport { config } from '#load-config';\n\nexport const TOKEN_EXPIRATION_NEVER = -1;\nconst MS_PER_SECOND = 1000;\n\nexport function validateSession(req: Request, res: Response) {\n  let { token } = req.body || {};\n\n  if (!token) {\n    token = req.headers['x-actual-token'];\n  }\n\n  const session = getSession(token);\n\n  if (!session) {\n    res.status(401);\n    res.send({\n      status: 'error',\n      reason: 'unauthorized',\n      details: 'token-not-found',\n    });\n    return null;\n  }\n\n  if (\n    session.expires_at !== TOKEN_EXPIRATION_NEVER &&\n    session.expires_at * MS_PER_SECOND <= Date.now()\n  ) {\n    res.status(401);\n    res.send({\n      status: 'error',\n      reason: 'token-expired',\n    });\n    return null;","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/validate-user.ts#L2-L38","documentation":"The sync-server rejects the request because the session token presented by the client does not exist in the account database. validateSession reads the token from the request body or the x-actual-token header, looks it up via getSession, and returns a 401 with details 'token-not-found' when no matching row is found. This means the server has no record of ever issuing that token (or it was deleted, e.g. by a logout that clears sessions).","triggerScenarios":"Calling any sync-server endpoint authenticated by validateSession (e.g. /sync, /download, /list-user-files) with: (1) no x-actual-token header and no token in the body, (2) a token string that was never issued by this server (wrong server URL, server wiped its account.db), or (3) a token invalidated by a password change / session cleanup on the server.","commonSituations":"Client pointing at the wrong sync-server instance (self-hosted vs hosted); server database re-created or migrated while clients kept old tokens; reverse-proxy stripping the x-actual-token header; hardcoding a token from a dev environment into production config.","solutions":["Re-authenticate the client: log in again via /login (or the app's sign-in screen) to obtain a fresh session token.","Verify the client is pointed at the correct syncServerURL and that the server's account.db still contains the session (SELECT * FROM sessions).","Ensure the x-actual-token header is forwarded by any reverse proxy in front of the server.","If the server database was reset, users must log in again; restore account.db from backup if old tokens must keep working."],"exampleFix":"// before: request without token\nawait fetch(url + '/sync', { method: 'POST', body });\n// after: attach token\nconst headers = { 'x-actual-token': token, 'Content-Type': 'application/json' };\nawait fetch(url + '/sync', { method: 'POST', headers, body });","handlingStrategy":"try-catch","validationCode":"const token = body.token || headers['x-actual-token'];\nif (!token) throw new Error('x-actual-token header or body token is required before calling the sync server');","typeGuard":"function hasToken(req): req is Request & { token: string } {\n  return typeof (req.body?.token ?? req.headers['x-actual-token']) === 'string' &&\n    (req.body?.token ?? req.headers['x-actual-token']).length > 0;\n}","tryCatchPattern":"try {\n  const res = await fetch(url + '/sync', { headers: { 'x-actual-token': token } });\n  const data = await res.json();\n  if (res.status === 401 && data.details === 'token-not-found') {\n    await reauthenticate(); // obtain fresh token and retry once\n  }\n} catch (e) {\n  logger.error('sync request failed', e);\n}","preventionTips":["Always re-login after a server database reset or migration.","Store the token centrally and refresh it on any 401 instead of caching indefinitely.","Confirm reverse proxies forward the x-actual-token header.","Pin the syncServerURL per environment to avoid pointing clients at the wrong server."],"tags":["auth","sync-server","http-401","session-token"],"backgroundTag":"session-token-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}