{"record":{"id":"b69a5fa96cafa933","repo":"toeverything/AFFiNE","slug":"doc-update-blocked","errorCode":"doc_update_blocked","errorMessage":"Doc ${docId} under Space ${spaceId} is blocked from updating.","messagePattern":"Doc (.+?) under Space (.+?) is blocked from updating\\.","errorType":"exception","errorClass":"DocUpdateBlocked","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/sync/gateway.ts","lineNumber":1532,"sourceCode":"    private readonly docReader: DocReader,\n    private readonly models: Models\n  ) {\n    super(SpaceType.Workspace, client, storage);\n  }\n\n  override async push(\n    spaceId: string,\n    docId: string,\n    updates: Buffer[],\n    editorId: string\n  ) {\n    const docMeta = await this.models.doc.getMeta(spaceId, docId, {\n      select: {\n        blocked: true,\n      },\n    });\n    if (docMeta?.blocked) {\n      throw new DocUpdateBlocked({ spaceId, docId });\n    }\n    return await super.push(spaceId, docId, updates, editorId);\n  }\n\n  override async diff(\n    spaceId: string,\n    docId: string,\n    stateVector?: Uint8Array\n  ) {\n    return await this.docReader.getDocDiff(spaceId, docId, stateVector);\n  }\n\n  async assertAccessible(\n    spaceId: string,\n    userId: string,\n    action: WorkspaceAction\n  ) {\n    await this.ac.user(userId).workspace(spaceId).assert(action);","sourceCodeStart":1514,"sourceCodeEnd":1550,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b6de0ad51b76f3daac2d3d6325369ea623ed7ed4/packages/backend/server/src/core/sync/gateway.ts#L1514-L1550","documentation":"WorkspaceSyncAdapter.push loads the doc's metadata before persisting updates and throws doc_update_blocked when the blocked flag is true. A blocked doc is server-side write-locked (for example by moderation or a restore/maintenance flow): reads such as diff keep working, but new update pushes are rejected.","triggerScenarios":"Pushing doc updates via the sync gateway while models.doc.getMeta(spaceId, docId) has blocked=true; a maintenance/restore job flipping the flag while clients sync; admin tooling that blocks a doc.","commonSituations":"Self-hosted instance in the middle of a doc migration or backup restore; a doc flagged for review; clients misreading the rejection as a network error and retry-storming.","solutions":["Stop retrying pushes for that doc immediately - the block is an intentional server state, not a transient fault","Buffer the rejected updates locally (Yjs updates merge safely) and flush them once the doc is unblocked","Check the doc's blocked state through the doc metadata/API to detect when writes are allowed again","If you operate the server, finish or cancel the maintenance operation that set the flag"],"exampleFix":"// before\nawait adapter.push(spaceId, docId, updates, editorId);\n\n// after\ntry {\n  await adapter.push(spaceId, docId, updates, editorId);\n} catch (e) {\n  if (e.code === 'doc_update_blocked') {\n    pendingUpdates.push(...updates); // buffer, retry when unblocked\n  } else {\n    throw e;\n  }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await push(spaceId, docId, updates, editorId);\n} catch (e) {\n  if (e?.code === 'doc_update_blocked') {\n    pendingUpdates.push(...updates); // buffer Yjs updates locally\n    scheduleRetryWhenUnblocked(spaceId, docId); // e.g. poll doc metadata\n  } else {\n    throw e;\n  }\n}","preventionTips":["Never retry doc_update_blocked on a timer - it is a deliberate server state, not a transient error","Keep local update buffers durable so blocked-period updates are not lost on refresh","Surface a 'doc is locked' state in the UI instead of silently dropping edits","For servers you operate, announce maintenance windows that block docs"],"tags":["sync","write-lock","maintenance","yjs"],"backgroundTag":"document-write-locked","analyzedSha":"b6de0ad51b76f3daac2d3d6325369ea623ed7ed4","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}