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

  1. Catch ClientRequestedMissingOpsError in checkDocument/connection and force the client to reload the document from source (full resync) instead of applying deltas.
  2. Trigger a document flush + reload on document-updater so the doc is reloaded from mongo with fresh ops.
  3. Check redis TTL/eviction settings for document-updater if clients routinely request ops that were evicted.
  4. 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

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


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/f9a3567288eb9d64. Report an issue: GitHub.