{"record":{"id":"707c680b6371ccf0","repo":"actualbudget/actual","slug":"text","errorCode":null,"errorMessage":"${text}","messagePattern":"\\$\\{text\\}","errorType":"http","errorClass":"HTTPError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/cloud-storage.ts","lineNumber":67,"sourceCode":"async function checkHTTPStatus(res) {\n  if (res.status === 200) {\n    return res;\n  }\n\n  const text = await res.text();\n  if (res.status === 401 || res.status === 403) {\n    try {\n      const body = JSON.parse(text);\n      const error = res.status === 403 ? body.data : body;\n      if (getServerErrorReason(error) === 'token-expired') {\n        await asyncStorage.removeItem('user-token');\n      }\n    } catch {\n      // Preserve the original HTTP error when the response is not JSON.\n    }\n  }\n\n  throw new HTTPError(res.status, text);\n}\n\nasync function fetchJSON(...args: Parameters<typeof fetch>) {\n  let res = await fetch(...args);\n  res = await checkHTTPStatus(res);\n  return res.json();\n}\n\nexport async function checkKey(): Promise<{\n  valid: boolean;\n  error?: { reason: string };\n}> {\n  const userToken = await asyncStorage.getItem('user-token');\n\n  const { cloudFileId, encryptKeyId } = prefs.getPrefs();\n\n  let res;\n  try {","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/cloud-storage.ts#L49-L85","documentation":"checkHTTPStatus inspects a fetch response against the cloud storage server; when the status is not OK it throws an HTTPError carrying the status code and, when the body is not JSON, the raw response text as the message. fetchJSON always routes responses through it before parsing JSON.","triggerScenarios":"Any cloud-storage call made through fetchJSON (upload/download files, get/create budget, etc.) where the server returns a non-2xx status: 401 for an expired token, 404 for an unknown budget id, 409/500 from the sync server, or a proxy returning an HTML/text error page.","commonSituations":"Self-hosted sync server behind an auth-reverse-proxy returning 401 text; wrong server URL hitting a random site's 404; server restarted and returning 502 from a load balancer; expired login token.","solutions":["Read the HTTPError status and text in the message to identify the cause (401/403 auth, 404 wrong id/URL, 5xx server fault).","Verify the server URL in Settings points at the correct sync-server root and that the server is running (curl the /status endpoint).","Re-authenticate: sign out/in to refresh the token if the status is 401/403.","If the body is an HTML page, a proxy/load balancer is intercepting — check reverse-proxy config and upstream health.","Retry later or inspect sync-server logs for 5xx causes."],"exampleFix":"// before\nconst res = await fetchJSON(url); // 404 HTML page -> throws with raw text\n// after\ntry {\n  const res = await fetchJSON(url);\n} catch (e) {\n  if (e instanceof HTTPError && e.status === 404) {\n    throw new Error(`Budget not found at ${url}; check server URL and budget id`);\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url);\nif (!res.ok) {\n  throw new Error(`Server returned ${res.status} before sync: ${await res.text()}`);\n}\nawait res.body?.cancel();","typeGuard":"function isHTTPError(e: unknown): e is { status: number; message: string } {\n  return e instanceof Error && 'status' in e && typeof (e as any).status === 'number';\n}","tryCatchPattern":"try {\n  const data = await fetchJSON(url);\n} catch (e) {\n  if (isHTTPError(e)) {\n    if (e.status === 401 || e.status === 403) await reauthenticate();\n    else if (e.status === 404) console.error('Check server URL / budget id');\n    else console.error(`Server error ${e.status}; retry later`, e.message);\n  } else throw e;\n}","preventionTips":["Confirm the sync server is reachable (curl <server>/status) before operations.","Refresh auth tokens when 401s appear; sessions expire.","Check reverse-proxy config if responses are HTML instead of JSON.","Log the status code and body text for diagnosis before retrying.","Verify the server URL matches the running sync-server exactly."],"tags":["http","network","cloud-storage","fetch"],"backgroundTag":"http-error-response","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}