actualbudget/actual · error

since-required

since-required

Error message

since-required

What it means

The sync protocol requires a 'since' timestamp cursor to know from which point to return changes. If the decoded SyncRequest has no since value, the server responds 422 with details 'since-required' and reason 'unprocessable-entity'.

Source

Thrown at packages/sync-server/src/app-sync.ts:156

app.post('/sync', async (req, res): Promise<void> => {
  let requestPb;
  try {
    requestPb = fromBinary(SyncRequestSchema, req.body);
  } catch (e) {
    console.log('Error parsing sync request', e);
    res.status(500);
    res.send({ status: 'error', reason: 'internal-error' });
    return;
  }

  const fileId = requestPb.fileId || null;
  const groupId = requestPb.groupId || null;
  const keyId = requestPb.keyId || null;
  const since = requestPb.since || null;
  const messages = requestPb.messages;

  if (!since) {
    res.status(422).send({
      details: 'since-required',
      reason: 'unprocessable-entity',
      status: 'error',
    });
    return;
  }

  const filesService = new FilesService(getAccountDb());

  const currentFile = verifyFileExists(
    fileId,
    filesService,
    res,
    'file-not-found',
  );

  if (!currentFile) {
    return;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Set the since field on the SyncRequest to the last received sync timestamp (use '0' for an initial full sync).
  2. Re-generate the client protobuf code from the current schema so since is populated.
  3. If using the official client/API, upgrade it — it sets since automatically.

Example fix

// before
const request = { fileId, groupId, messages };
// after
const request = { fileId, groupId, messages, since: lastSyncedTimestamp || '0' };
Defensive patterns

Strategy: validation

Validate before calling

if (!request.since || request.since === '0' && !allowFullSync) throw new Error('since is required; pass the last sync timestamp or 0 for initial sync');

Type guard

const hasSince = (r) => typeof r.since === 'string' && r.since.length > 0;

Try / catch

try {
  const res = await postSync(request);
  if (res.status === 422 && (await res.json()).details === 'since-required') {
    throw new Error('add a since cursor to the SyncRequest');
  }
  return res;
} catch (e) { throw e; }

Prevention

When it happens

Trigger: POSTing a well-formed SyncRequest protobuf whose since field is 0/null/absent — e.g. hand-built sync requests or clients that omitted the field after a protocol change.

Common situations: Custom API integrations constructing SyncRequest manually; clients upgraded against an older schema where since was optional; resetting local state and forgetting to seed the cursor (use '0' or the epoch value for a full initial sync).

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/b24261430220dda6. Report an issue: GitHub.