{"record":{"id":"607f2cf6c3496283","repo":"mozilla/pdf.js","slug":"getoperatorlist-ignoring-circular-reference-o","errorCode":null,"errorMessage":"getOperatorList - ignoring circular reference: ${objId}","messagePattern":"getOperatorList - ignoring circular reference: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/evaluator.js","lineNumber":1695,"sourceCode":"    resources,\n    operatorList,\n    initialState = null,\n    fallbackFontDict = null,\n    prevRefs = null,\n  }) {\n    if (stream.isAsync) {\n      const bytes = await stream.asyncGetBytes();\n      if (bytes) {\n        stream = new Stream(bytes, 0, bytes.length, stream.dict);\n      }\n    }\n\n    const objId = stream.dict?.objId;\n    const seenRefs = new RefSet(prevRefs);\n\n    if (objId) {\n      if (prevRefs?.has(objId)) {\n        throw new Error(\n          `getOperatorList - ignoring circular reference: ${objId}`\n        );\n      }\n      seenRefs.put(objId);\n    }\n    // Ensure that `resources`/`initialState` is correctly initialized,\n    // even if the provided parameter is e.g. `null`.\n    resources ||= Dict.empty;\n    initialState ||= new EvalState();\n\n    if (!operatorList) {\n      throw new Error('getOperatorList: missing \"operatorList\" parameter');\n    }\n\n    const self = this;\n    const xref = this.xref;\n    const localImageCache = new LocalImageCache();\n    const localColorSpaceCache = new LocalColorSpaceCache();","sourceCodeStart":1677,"sourceCodeEnd":1713,"githubUrl":"https://github.com/mozilla/pdf.js/blob/5903d58d58e4dd9ce6ffa3834aea8480f06b4ada/src/core/evaluator.js#L1677-L1713","documentation":"Thrown at the very start of getOperatorList when the stream's objId already appears in prevRefs (a RefSet tracking ancestor streams). It is an anti-recursion guard: a Form XObject or content stream that (directly or transitively) references itself would otherwise loop forever. Unlike operator-dispatch errors, this is a bare throw at method entry and is NOT suppressed by ignoreErrors.","triggerScenarios":"buildFormXObject calls getOperatorList with prevRefs including the current objId, which happens when a Form XObject's stream chain re-enters a stream already on the ancestor path (cycle in the XObject graph).","commonSituations":"Malformed or maliciously crafted PDFs with cyclic Form XObject references; PDFs damaged in transit so that object references form a loop.","solutions":["Wrap the page.render() / getOperatorList call in try/catch and present a graceful 'page could not be rendered' result, since ignoreErrors will not help here.","Repair the PDF to break the cycle in the XObject reference graph.","Upgrade PDF.js; cycle-detection has been hardened over releases."],"exampleFix":"// before\nconst page = await pdf.getPage(n);\nawait page.render({ canvasContext, viewport }).promise; // rejects on cycle\n\n// after\ntry {\n  await page.render({ canvasContext, viewport }).promise;\n} catch (e) {\n  console.warn('Unrenderable page (possible circular XObject):', e.message);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// ignoreErrors does NOT suppress this; catch at the render boundary.\ntry {\n  await page.render({ canvasContext, viewport }).promise;\n} catch (e) {\n  if (/circular reference/.test(e.message)) {\n    showUnrenderablePagePlaceholder(page.pageNumber);\n  } else throw e;\n}","preventionTips":["Never assume ignoreErrors catches cycle guards; it does not.","Run incoming PDFs through a repair/sanitize pipeline to break reference loops.","Give every page render a top-level try/catch in production viewers."],"tags":["pdf","circular-reference","xobject","malformed-pdf","rendering"],"backgroundTag":null,"analyzedSha":"5903d58d58e4dd9ce6ffa3834aea8480f06b4ada","analyzedAt":"2026-08-13T02:28:27.364Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}