toeverything/AFFiNE · error · InvalidHistoryTimestamp

invalid_history_timestamp

invalid_history_timestamp

Error message

Invalid doc history timestamp provided.

What it means

Intended to be thrown by WorkspacesController.history when the :timestamp path param cannot be parsed into a Date. As written the guard is ineffective: new Date(string) never throws — it returns an Invalid Date — so the catch block is effectively dead code and a bad timestamp flows through as NaN ms, usually surfacing later as doc_history_not_found instead of this error.

Solutions

  1. Pass a value Date can parse: epoch milliseconds or a full ISO 8601 string
  2. Validate client-side first: const ms = Date.parse(ts); if (Number.isNaN(ms)) stop before calling
  3. Server maintainers: replace the try/catch with an explicit isNaN(ts.getTime()) check so invalid input actually returns invalid_history_timestamp
  4. If you received doc_history_not_found for an odd timestamp, suspect this dead guard and re-check the param

Example fix

// before (controller.ts:350)
let ts;
try {
  ts = new Date(timestamp);
} catch {
  throw new InvalidHistoryTimestamp({ timestamp });
}
// after
const ts = new Date(timestamp);
if (Number.isNaN(ts.getTime())) {
  throw new InvalidHistoryTimestamp({ timestamp });
}
Defensive patterns

Strategy: validation

Validate before calling

export function toValidHistoryTimestamp(input: string): number | null {
  const ms = Date.parse(input); // handles ISO 8601; numeric strings need +input
  if (!Number.isNaN(ms)) return ms;
  const asEpoch = Number(input);
  return Number.isFinite(asEpoch) ? asEpoch : null;
}

const ts = toValidHistoryTimestamp(timestampParam);
if (ts === null) {
  showInvalidTimestampError(timestampParam);
} else {
  await fetchHistory(wsId, guid, ts);
}

Type guard

function isValidTimestamp(v: string): v is string {
  return Number.isFinite(Date.parse(v)) || Number.isFinite(Number(v));
}

Try / catch

try {
  return await getDocHistory(wsId, guid, timestamp);
} catch (e) {
  if (e?.code === 'invalid_history_timestamp') {
    return promptForValidTimestamp(); // client bug: bad param format
  }
  throw e;
}

Prevention

When it happens

Trigger: GET /api/workspaces/:ws/docs/:guid/histories/:timestamp with a non-parseable value ('latest', '2024-13-45', empty). Only a Date constructor that actually throws would trigger it, which string inputs do not.

Common situations: Clients sending a human label instead of epoch ms / ISO 8601; URL encoding mangling the timestamp; format changes breaking old copied links; tests asserting this error and mysteriously seeing doc_history_not_found instead.

Related errors


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

Appendix: source

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

    res.setHeader('content-type', 'application/octet-stream');
    res.send(publicRootDoc);
  }

  @Get('/:id/docs/:guid/histories/:timestamp')
  @CallMetric('controllers', 'workspace_get_history')
  async history(
    @CurrentUser() user: CurrentUser,
    @Param('id') ws: string,
    @Param('guid') guid: string,
    @Param('timestamp') timestamp: string,
    @Res() res: Response
  ) {
    const docId = new DocID(guid, ws);
    let ts;
    try {
      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,

View on GitHub (pinned to b4c8548c09)