overleaf/overleaf · error · BaseVersionConflictError

base version mismatch

Error message

base version mismatch

What it means

queueChanges pushes changes onto the Redis pending-changes list guarded by the project's head version. When the Lua script reports status 'conflict', the caller's baseVersion does not match the current head version, and a BaseVersionConflictError is thrown with both values attached. This is the store's optimistic-concurrency mechanism protecting the change sequence.

Source

Thrown at services/history-v1/storage/lib/chunk_store/redis.js:210

    const args = [
      baseVersion.toString(),
      JSON.stringify(headSnapshot.toRaw()),
      persistTime.toString(),
      expireTime.toString(),
      onlyIfExists.toString(), // Only queue changes if the snapshot already exists
      ...changes.map(change => JSON.stringify(change.toRaw())), // Serialize changes
    ]

    const status = await rclient.queue_changes(keys, args)
    metrics.inc('chunk_store.redis.queue_changes', 1, { status })
    if (status === 'ok') {
      return status
    }
    if (status === 'ignore') {
      return status // skip changes when project does not exist and onlyIfExists is true
    }
    if (status === 'conflict') {
      throw new BaseVersionConflictError('base version mismatch', {
        projectId,
        baseVersion,
      })
    } else {
      throw new OError('unexpected result queuing changes', { status })
    }
  } catch (err) {
    if (err instanceof BaseVersionConflictError) {
      // Re-throw conflict errors directly
      throw err
    }
    metrics.inc('chunk_store.redis.queue_changes', 1, { status: 'error' })
    throw err
  }
}

rclient.defineCommand('get_state', {
  numberOfKeys: 6, // Number of keys defined in keySchema

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Catch BaseVersionConflictError, reload the latest head snapshot, rebase the changes onto it, and retry queueChanges with the new baseVersion.
  2. Before queuing, fetch the current head version and refuse/refresh when it differs from your baseVersion.
  3. Ensure each client session refreshes its base version after any conflict or reconnect instead of retrying blindly.

Example fix

// before
await queueChanges(projectId, snapshot, baseVersion, changes)

// after
try {
  await queueChanges(projectId, snapshot, baseVersion, changes)
} catch (err) {
  if (err instanceof BaseVersionConflictError) {
    const fresh = await chunkStore.loadHead(projectId)
    const rebased = rebaseChanges(changes, snapshot, fresh.getSnapshot())
    await queueChanges(projectId, fresh.getSnapshot(), fresh.getVersion(), rebased)
  } else {
    throw err
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const head = await chunkStore.getProjectHeadVersion(projectId); if (head !== baseVersion) { await refreshBaseSnapshot(); }

Type guard

function isBaseVersionConflict(err) { return err instanceof BaseVersionConflictError }

Try / catch

try { await queueChanges(projectId, snapshot, baseVersion, changes) } catch (err) { if (isBaseVersionConflict(err)) { await rebaseAndRetry(projectId, changes, err.info) } else { throw err } }

Prevention

When it happens

Trigger: Calling queueChanges with a baseVersion computed from a stale head snapshot while another client has already appended changes; two editors publishing concurrently from the same base version.

Common situations: Long-lived editor sessions whose head version fell behind after reconnects; a second tab/device editing the same project; a retry of an old queued batch after the version advanced.

Related errors


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