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 WorkspacesController.history when workspace.getDocHistory(workspaceId, guid, ts.getTime()) returns nothing — permission passed but no history snapshot exists at that timestamp. Served with private immutable caching when found; this error is the empty-case 404.

Solutions

  1. List available history entries first and use an exact timestamp from that list
  2. Verify the snapshot/history job actually runs for the workspace
  3. Pick a timestamp after the doc's creation time
  4. In clients, catch this and fall back to the current doc binary
Defensive patterns

Strategy: validation

Validate before calling

// choose from timestamps the server actually has, instead of guessing
const histories = await listDocHistories(wsId, guid); // available snapshots
if (histories.length === 0) {
  showNoHistoryAvailable(guid);
  return;
}
const target = histories.find(h => Math.abs(h.timestamp - wanted) < 60_000) ?? histories[0];
return fetchHistory(wsId, guid, target.timestamp);

Type guard

function isDocHistoryNotFound(e: unknown): e is { code: 'doc_history_not_found'; docId: string; spaceId: string; timestamp: number } {
  return typeof e === 'object' && e !== null && (e as any).code === 'doc_history_not_found';
}

Try / catch

try {
  return await getDocHistory(wsId, guid, ts);
} catch (e) {
  if (isDocHistoryNotFound(e)) {
    return getCurrentDocBinary(wsId, guid); // fall back to latest state
  }
  throw e;
}

Prevention

When it happens

Trigger: GET /api/workspaces/:id/docs/:guid/histories/:timestamp for a timestamp where no snapshot was captured: before the doc's first snapshot, after retention cleaned histories, snapshotting disabled for the workspace, or NaN ms from an unparseable timestamp (see the dead guard on the previous line).

Common situations: Using an arbitrary or rounded timestamp instead of one from the history list; clock-skewed client timestamps; snapshot cron never ran on self-host; timestamp predates doc creation.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/165e3f218bd15235. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/controller.ts:366

      ts = new Date(timestamp);
    } catch {
      throw new InvalidHistoryTimestamp({ timestamp });
    }

    await this.ac.user(user.id).doc(ws, guid).assert('Doc.Read');

    const history = await this.workspace.getDocHistory(
      docId.workspace,
      docId.guid,
      ts.getTime()
    );

    if (history) {
      res.setHeader('content-type', 'application/octet-stream');
      res.setHeader('cache-control', 'private, max-age=2592000, immutable');
      res.send(history.bin);
    } else {
      throw new DocHistoryNotFound({
        spaceId: docId.workspace,
        docId: guid,
        timestamp: ts.getTime(),
      });
    }
  }

  @Get('/:id/docs/:docId/comment-attachments/:key')
  @CallMetric('controllers', 'workspace_get_comment_attachment')
  async commentAttachment(
    @CurrentUser() user: CurrentUser,
    @Param('id') workspaceId: string,
    @Param('docId') docId: string,
    @Param('key') key: string,
    @Res() res: Response
  ) {
    await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Read');

View on GitHub (pinned to b4c8548c09)