overleaf/overleaf · error · ClientRequestedMissingOpsError
doc updater could not load requested ops
Error message
doc updater could not load requested ops
What it means
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.
Source
Thrown at services/real-time/app/js/DocumentUpdaterManager.js:55
lines: body?.lines,
version: body?.version,
ranges: body?.ranges,
ops: body?.ops,
ttlInS: body?.ttlInS,
type: body?.type,
}
} catch (err) {
timer.done()
if (err instanceof RequestFailedError) {
const { response, body } = err
let parsedErrBody = null
try {
parsedErrBody = JSON.parse(body)
} catch (error) {
// ignore parse error
}
if (response.status === 422 && parsedErrBody?.firstVersionInRedis) {
throw new ClientRequestedMissingOpsError(422, parsedErrBody)
} else if ([404, 422].includes(response.status)) {
throw new ClientRequestedMissingOpsError(response.status)
} else {
throw new DocumentUpdaterRequestFailedError(
'getDocument',
response.status
)
}
}
OError.tag(err, 'error getting doc from doc updater')
throw err
}
}
async function checkDocument(projectId, docId) {
// in this call fromVersion = -1 means get document without docOps
return await getDocument(projectId, docId, -1)
}View on GitHub (pinned to 28ad3b03b7)
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.
Example fix
// before
await DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)
// after
try {
await DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)
} catch (err) {
if (err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError) {
return joinAtCurrentVersion(projectId, docId)
}
throw err
} Defensive patterns
Strategy: try-catch
Validate before calling
if (clientLastVersion < doc.firstVersionInRedis) return fullDocumentReload(docId)
Type guard
const isMissingOpsError = err => err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError || err?.name === 'ClientRequestedMissingOpsError'
Try / catch
try {
await DocumentUpdaterManager.promises.getDocument(projectId, docId, fromVersion)
} catch (err) {
if (err instanceof DocumentUpdaterManager.ClientRequestedMissingOpsError) {
return sendFullDocumentReload(client, projectId, docId)
}
throw err
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- refusing to append to file, it would become too large
- doc would become too large if appending this text
- document not found: ${docId}
- comment not found
- document not found: ${docId}
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/f9a3567288eb9d64.
Report an issue: GitHub.