{"record":{"id":"b251d365413f729c","repo":"toeverything/AFFiNE","slug":"invalid-history-timestamp","errorCode":"invalid_history_timestamp","errorMessage":"Invalid doc history timestamp provided.","messagePattern":"Invalid doc history timestamp provided\\.","errorType":"exception","errorClass":"InvalidHistoryTimestamp","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/controller.ts","lineNumber":350,"sourceCode":"    res.setHeader('content-type', 'application/octet-stream');\n    res.send(publicRootDoc);\n  }\n\n  @Get('/:id/docs/:guid/histories/:timestamp')\n  @CallMetric('controllers', 'workspace_get_history')\n  async history(\n    @CurrentUser() user: CurrentUser,\n    @Param('id') ws: string,\n    @Param('guid') guid: string,\n    @Param('timestamp') timestamp: string,\n    @Res() res: Response\n  ) {\n    const docId = new DocID(guid, ws);\n    let ts;\n    try {\n      ts = new Date(timestamp);\n    } catch {\n      throw new InvalidHistoryTimestamp({ timestamp });\n    }\n\n    await this.ac.user(user.id).doc(ws, guid).assert('Doc.Read');\n\n    const history = await this.workspace.getDocHistory(\n      docId.workspace,\n      docId.guid,\n      ts.getTime()\n    );\n\n    if (history) {\n      res.setHeader('content-type', 'application/octet-stream');\n      res.setHeader('cache-control', 'private, max-age=2592000, immutable');\n      res.send(history.bin);\n    } else {\n      throw new DocHistoryNotFound({\n        spaceId: docId.workspace,\n        docId: guid,","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/workspaces/controller.ts#L332-L368","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (controller.ts:350)\nlet ts;\ntry {\n  ts = new Date(timestamp);\n} catch {\n  throw new InvalidHistoryTimestamp({ timestamp });\n}\n// after\nconst ts = new Date(timestamp);\nif (Number.isNaN(ts.getTime())) {\n  throw new InvalidHistoryTimestamp({ timestamp });\n}","handlingStrategy":"validation","validationCode":"export function toValidHistoryTimestamp(input: string): number | null {\n  const ms = Date.parse(input); // handles ISO 8601; numeric strings need +input\n  if (!Number.isNaN(ms)) return ms;\n  const asEpoch = Number(input);\n  return Number.isFinite(asEpoch) ? asEpoch : null;\n}\n\nconst ts = toValidHistoryTimestamp(timestampParam);\nif (ts === null) {\n  showInvalidTimestampError(timestampParam);\n} else {\n  await fetchHistory(wsId, guid, ts);\n}","typeGuard":"function isValidTimestamp(v: string): v is string {\n  return Number.isFinite(Date.parse(v)) || Number.isFinite(Number(v));\n}","tryCatchPattern":"try {\n  return await getDocHistory(wsId, guid, timestamp);\n} catch (e) {\n  if (e?.code === 'invalid_history_timestamp') {\n    return promptForValidTimestamp(); // client bug: bad param format\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["validation","timestamp","history","date-parsing"],"backgroundTag":"date-parse-failed","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}