{"record":{"id":"57b86a39dedb4f13","repo":"actualbudget/actual","slug":"internal-error-57b86a","errorCode":"internal-error","errorMessage":"internal-error","messagePattern":"internal-error","errorType":"http","errorClass":null,"httpStatus":500,"severity":"critical","filePath":"packages/sync-server/src/app-sync.ts","lineNumber":144,"sourceCode":"}\n\nfunction requireFileAccess(file: File, userId: string) {\n  if (requireFileOwner(file, userId) === null) {\n    return null;\n  }\n  if (UserService.countUserAccess(file.id, userId) > 0) {\n    return null;\n  }\n  return 'file-access-not-allowed';\n}\n\napp.post('/sync', async (req, res): Promise<void> => {\n  let requestPb;\n  try {\n    requestPb = fromBinary(SyncRequestSchema, req.body);\n  } catch (e) {\n    console.log('Error parsing sync request', e);\n    res.status(500);\n    res.send({ status: 'error', reason: 'internal-error' });\n    return;\n  }\n\n  const fileId = requestPb.fileId || null;\n  const groupId = requestPb.groupId || null;\n  const keyId = requestPb.keyId || null;\n  const since = requestPb.since || null;\n  const messages = requestPb.messages;\n\n  if (!since) {\n    res.status(422).send({\n      details: 'since-required',\n      reason: 'unprocessable-entity',\n      status: 'error',\n    });\n    return;\n  }","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-sync.ts#L126-L162","documentation":"The /sync endpoint parses the request body as a binary protobuf (SyncRequest). If fromBinary fails — the body is not valid SyncRequest protobuf — the server logs the error and returns 500 with reason 'internal-error'. Note this is a client payload problem surfaced as a server error.","triggerScenarios":"POSTing to /sync with a non-protobuf body (JSON, form data), a truncated/corrupted protobuf, a SyncRequest serialized against an incompatible schema version, or a missing Content-Type so express does not deliver the raw buffer to fromBinary.","commonSituations":"Custom scripts hand-rolling sync calls; client and server versions mismatched after an upgrade; proxies/CDNs mangling the binary body; forgetting the correct express raw-body parser configuration in front of the app.","solutions":["Serialize the request with toBinary(SyncRequestSchema, request) using the same protobuf schema version as the server and send it as the raw body with an appropriate binary content type.","Upgrade/align the client (@actual-app/api or desktop-client) with the sync-server version.","Check intermediate proxies are not altering the body, and verify the server mounts the raw-body parser for /sync."],"exampleFix":"// before: sending JSON to /sync\nawait fetch(serverUrl + '/sync', { method: 'POST', body: JSON.stringify(request) }); // 500 internal-error\n// after: send the protobuf binary\nimport { toBinary, SyncRequestSchema } from './proto/sync';\nconst body = toBinary(SyncRequestSchema, request);\nawait fetch(serverUrl + '/sync', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body });","handlingStrategy":"try-catch","validationCode":"import { toBinary, SyncRequestSchema } from './sync.proto';\nconst body = toBinary(SyncRequestSchema, request); // fail fast locally if the shape is wrong\nif (!(body instanceof Uint8Array) || body.length === 0) throw new Error('sync body must be a non-empty protobuf');","typeGuard":"const isSyncRequest = (r) => typeof r.fileId === 'string' && Array.isArray(r.messages);","tryCatchPattern":"try {\n  const res = await fetch(serverUrl + '/sync', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body });\n  if (res.status === 500) {\n    const j = await res.json();\n    if (j.reason === 'internal-error') throw new Error('server could not parse the sync request: check protobuf schema/version');\n  }\n  return res;\n} catch (e) { throw e; }","preventionTips":["Always send protobuf produced by toBinary, never JSON, to /sync","Keep client and sync-server versions aligned","Set Content-Type to a binary type and avoid proxies that rewrite bodies"],"tags":["protobuf","http-500","sync","request-format"],"backgroundTag":"invalid-request-payload","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}