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
- Pass a value Date can parse: epoch milliseconds or a full ISO 8601 string
- Validate client-side first: const ms = Date.parse(ts); if (Number.isNaN(ms)) stop before calling
- Server maintainers: replace the try/catch with an explicit isNaN(ts.getTime()) check so invalid input actually returns invalid_history_timestamp
- 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
- Always send epoch milliseconds or full ISO 8601 — never labels or partial dates
- Validate timestamps client-side with Date.parse/Number.isNaN before the request
- URL-encode path params so timestamps aren't mangled
- Server maintainers: replace the try/catch around new Date with an isNaN(ts.getTime()) check — the current guard never fires
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)