{"record":{"id":"f9a3567288eb9d64","repo":"overleaf/overleaf","slug":"doc-updater-could-not-load-requested-ops","errorCode":null,"errorMessage":"doc updater could not load requested ops","messagePattern":"doc updater could not load requested ops","errorType":"error_code","errorClass":"ClientRequestedMissingOpsError","httpStatus":422,"severity":"error","filePath":"services/real-time/app/js/DocumentUpdaterManager.js","lineNumber":55,"sourceCode":"      lines: body?.lines,\n      version: body?.version,\n      ranges: body?.ranges,\n      ops: body?.ops,\n      ttlInS: body?.ttlInS,\n      type: body?.type,\n    }\n  } catch (err) {\n    timer.done()\n    if (err instanceof RequestFailedError) {\n      const { response, body } = err\n      let parsedErrBody = null\n      try {\n        parsedErrBody = JSON.parse(body)\n      } catch (error) {\n        // ignore parse error\n      }\n      if (response.status === 422 && parsedErrBody?.firstVersionInRedis) {\n        throw new ClientRequestedMissingOpsError(422, parsedErrBody)\n      } else if ([404, 422].includes(response.status)) {\n        throw new ClientRequestedMissingOpsError(response.status)\n      } else {\n        throw new DocumentUpdaterRequestFailedError(\n          'getDocument',\n          response.status\n        )\n      }\n    }\n    OError.tag(err, 'error getting doc from doc updater')\n    throw err\n  }\n}\n\nasync function checkDocument(projectId, docId) {\n  // in this call fromVersion = -1 means get document without docOps\n  return await getDocument(projectId, docId, -1)\n}","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/real-time/app/js/DocumentUpdaterManager.js#L37-L73","documentation":"getDocument in real-time's DocumentUpdaterManager fetches a doc plus recent ops from document-updater. When document-updater answers 404 or 422 (and a 422 body lacks firstVersionInRedis), it throws ClientRequestedMissingOpsError('doc updater could not load requested ops', status). It signals the client requested ops (via fromVersion) that document-updater can no longer serve.","triggerScenarios":"A real-time client joining a doc with fromVersion older than the ops retained in redis (ops evicted after flush), or requesting a doc that no longer exists in document-updater's redis (404).","commonSituations":"User keeps a stale editor tab open for a long time while the doc was flushed to mongo, then reconnects and requests missing old ops; redis restart wiping document-updater state; race during project resync.","solutions":["Catch ClientRequestedMissingOpsError in checkDocument/connection and force the client to reload the document from source (full resync) instead of applying deltas.","Trigger a document flush + reload on document-updater so the doc is reloaded from mongo with fresh ops.","Check redis TTL/eviction settings for document-updater if clients routinely request ops that were evicted.","Join the client at the current version (ignore fromVersion) rather than replaying unavailable history."],"exampleFix":"// before\nawait DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)\n// after\ntry {\n  await DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)\n} catch (err) {\n  if (err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError) {\n    return joinAtCurrentVersion(projectId, docId)\n  }\n  throw err\n}","handlingStrategy":"try-catch","validationCode":"if (clientLastVersion < doc.firstVersionInRedis) return fullDocumentReload(docId)","typeGuard":"const isMissingOpsError = err => err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError || err?.name === 'ClientRequestedMissingOpsError'","tryCatchPattern":"try {\n  await DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)\n} catch (err) {\n  if (err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError) {\n    return sendFullDocumentReload(client, projectId, docId)\n  }\n  throw err\n}","preventionTips":["Clamp fromVersion to the oldest available op before requesting from document-updater.","Send clients a full snapshot when their version predates retained redis ops.","Monitor redis eviction/restarts that wipe document-updater op history.","Keep doc flush intervals short so ops stay available for reconnecting clients."],"tags":["real-time","document-updater","missing-ops","http-422"],"backgroundTag":"client-requested-missing-ops","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}