{"record":{"id":"cb18269b8615733f","repo":"overleaf/overleaf","slug":"unknown-scanop-type-during-apply","errorCode":null,"errorMessage":"Unknown ScanOp type during apply","messagePattern":"Unknown ScanOp type during apply","errorType":"validation","errorClass":"UnprocessableError","httpStatus":null,"severity":"error","filePath":"libraries/overleaf-editor-core/lib/operation/text_operation.js","lineNumber":321,"sourceCode":"          throw new ApplyError(\n            \"Operation can't retain more chars than are left in the string.\",\n            op.toJSON(),\n            str\n          )\n        }\n        result += str.slice(inputCursor, inputCursor + op.length)\n        inputCursor += op.length\n      } else if (op instanceof InsertOp) {\n        file.comments.applyInsert(\n          new Range(result.length, op.insertion.length),\n          { commentIds: op.commentIds }\n        )\n        result += op.insertion\n      } else if (op instanceof RemoveOp) {\n        file.comments.applyDelete(new Range(result.length, op.length))\n        inputCursor += op.length\n      } else {\n        throw new UnprocessableError('Unknown ScanOp type during apply')\n      }\n    }\n\n    if (inputCursor !== str.length) {\n      throw new TextOperation.ApplyError(\n        \"The operation didn't operate on the whole string.\",\n        operation,\n        str\n      )\n    }\n\n    if (result.length > TextOperation.MAX_STRING_LENGTH) {\n      throw new TextOperation.TooLongError(operation, result.length)\n    }\n\n    file.trackedChanges.applyTextOperation(this)\n\n    file.content = result","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/libraries/overleaf-editor-core/lib/operation/text_operation.js#L303-L339","documentation":"TextOperation.apply iterates its internal ScanOp list (RetainOp, InsertOp, RemoveOp) and throws UnprocessableError if it encounters a ScanOp instance it does not recognize. In practice this only happens if the internal op array was constructed with foreign or corrupted objects that bypass the normal builders/fromJSON validation.","triggerScenarios":"Manually pushing objects into the ops array (e.g. o.ops.push(new SomeOtherOp()) or plain objects instead of RetainOp/InsertOp/RemoveOp instances), or deserialization code that substitutes different op classes.","commonSituations":"Custom subclasses of scan ops not registered with apply's instanceof checks; monkey-patched or vendored library versions mixing classes across two copies of overleaf-editor-core (duplicate module instances so instanceof fails); test stubs injecting fake ops.","solutions":["Build operations only via TextOperation builders (retain/insert/remove) or fromJSON — never push raw objects into .ops","Check for duplicate copies of overleaf-editor-core in node_modules; class identity across two module instances breaks instanceof","Remove custom op subclasses or extend/patch apply to handle them","Log the offending element with JSON.stringify to identify its actual constructor"],"exampleFix":"// before\nop.ops.push({ length: 3 }) // plain object, not a ScanOp\n// after\nop.retain(3) // proper RetainOp instance","handlingStrategy":"validation","validationCode":"function hasOnlyKnownScanOps(op) {\n  return op.ops.every(o => o instanceof RetainOp || o instanceof InsertOp || o instanceof RemoveOp)\n}\n// guard: if (!hasOnlyKnownScanOps(op)) rebuild via fromJSON before apply","typeGuard":"function isScanOp(o) { return o instanceof RetainOp || o instanceof InsertOp || o instanceof RemoveOp }","tryCatchPattern":"try {\n  file.apply(op)\n} catch (err) {\n  if (err.name === 'UnprocessableError' && err.message.includes('Unknown ScanOp type')) {\n    logger.warn({ ops: op.ops.map(String) }, 'foreign op instance detected')\n    return TextOperation.fromJSON(op.toJSON())\n  }\n  throw err\n}","preventionTips":["Never push raw objects or third-party op classes into .ops","Ensure only one copy of overleaf-editor-core is bundled (check for duplicate module instances)","Round-trip unknown ops through toJSON/fromJSON to normalize class identity"],"tags":["ot","apply","internal"],"backgroundTag":"unknown-op-type","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}