toeverything/AFFiNE · error · DocHistoryNotFound

doc_history_not_found

doc_history_not_found

Error message

History of ${docId} at ${timestamp} under Space ${spaceId}.

What it means

Thrown by `rollbackDoc` when `getDocHistory(spaceId, docId, timestamp)` returns null — i.e., no history snapshot exists for that exact `timestamp`. The rollback cannot proceed without a target history record, so it aborts with `DocHistoryNotFound` (category `resource_not_found`), interpolating `docId`, `timestamp`, and `spaceId` into the message.

Source

Thrown at packages/backend/server/src/core/doc/adapters/workspace.ts:207

    return {
      spaceId: workspaceId,
      docId,
      bin: history.blob,
      timestamp: history.timestamp,
      editor: history.editor?.id,
    };
  }

  override async rollbackDoc(
    spaceId: string,
    docId: string,
    timestamp: number,
    editorId?: string
  ): Promise<void> {
    await using _lock = await this.lockDocForUpdate(spaceId, docId);
    const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
    if (!toSnapshot) {
      throw new DocHistoryNotFound({ spaceId, docId, timestamp });
    }

    const fromSnapshot = await this.getDocSnapshot(spaceId, docId);

    if (!fromSnapshot) {
      throw new DocNotFound({ spaceId, docId });
    }

    // force create a new history record after rollback
    await this.createDocHistory(
      {
        ...fromSnapshot,
        // override the editor to the one who requested the rollback
        editor: editorId,
      },
      true
    );
    // WARN:

View on GitHub (pinned to 26c515e050)

Solutions

  1. Re-fetch the available history list (`listDocHistories`) and only offer rollback to timestamps present in it.
  2. Verify the requested `timestamp` exactly matches a stored history row (not a client-approximated value).
  3. Check the workspace's history retention config (`historyMaxAge`) — expired history cannot be restored.
  4. Surface `doc_history_not_found` to the user as 'this history version is no longer available'.

Example fix

// before
await adapter.rollbackDoc(spaceId, docId, pickedTimestamp, editorId);

// after
const available = await adapter.listDocHistories(spaceId, docId, { limit: 50 });
if (!available.some(h => h.timestamp === pickedTimestamp)) {
  toast('This history version is no longer available.');
  return;
}
await adapter.rollbackDoc(spaceId, docId, pickedTimestamp, editorId);
Defensive patterns

Strategy: validation

Validate before calling

// Only offer rollback to timestamps present in the stored history
const history = await adapter.listDocHistories(spaceId, docId, { limit: 100 });
const validTimestamps = new Set(history.map(h => h.timestamp));

if (!validTimestamps.has(requestedTimestamp)) {
  toast('This history version is no longer available.');
  return;
}

await adapter.rollbackDoc(spaceId, docId, requestedTimestamp, editorId);

Type guard

function isHistoryEntry(value: unknown): value is { timestamp: number; blob: Buffer } {
  return typeof value === 'object' && value !== null &&
    typeof (value as { timestamp?: unknown }).timestamp === 'number';
}

Try / catch

try {
  await adapter.rollbackDoc(spaceId, docId, requestedTimestamp, editorId);
} catch (e) {
  if ((e as { code?: string }).code === 'doc_history_not_found') {
    toast('This history version is no longer available.');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Requesting a rollback to a `timestamp` for which no history row exists: the timestamp predates history retention, was pruned by `historyMaxAge`, was never snapshotted (interval not elapsed), or the doc has no history at all.

Common situations: History cleanup/expiry removing older rows; a client sending a stale timestamp from an old history list; workspaces whose `historyMaxAge`/`historyMinInterval` config suppresses frequent snapshots; rollback after the history table was trimmed.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/d0ac5fc86b4556e7. Report an issue: GitHub.