mjmlio/mjml · error · Error

Circular inclusion detected on file : ${partialPath}

Error message

Circular inclusion detected on file : ${partialPath}

What it means

handleInclude processes <mj-include path="..."> tags by reading the referenced partial. Before reading, it checks whether the partial's resolved path already appears in the current node's includedIn chain; if so, including it again would recurse forever, so it throws 'Circular inclusion detected on file : <path>'. The library detects include cycles at parse time rather than looping until stack overflow.

Source

Thrown at packages/mjml-parser-xml/src/index.js:235

      decoded.includes('\0') ||
      path.isAbsolute(decoded) ||
      hasDriveLetter(decoded) ||
      isUNCPath(decoded)
    ) {
      denyInclude(line)
      return
    }

    const partialPath = path.resolve(cwd, decoded)
    const curBeforeInclude = cur

    if (!isPathAllowed(partialPath)) {
      denyInclude(line)
      return
    }

    if (find(cur.includedIn, { file: partialPath }))
      throw new Error(`Circular inclusion detected on file : ${partialPath}`)

    let content

    try {
      content = fs.readFileSync(partialPath, 'utf8')
    } catch (e) {
      const newNode = {
        line,
        file,
        absoluteFilePath: path.resolve(cwd, actualPath),
        parent: cur,
        tagName: 'mj-raw',
        content: `<!-- mj-include fails to read file : ${file} at ${partialPath} -->`,
        children: [],
        errors: [
          {
            type: 'include',
            params: { file, partialPath },

View on GitHub (pinned to 6c01d35af5)

Solutions

  1. Inspect the cycle path in the message and break the loop: remove the <mj-include> that points back to an ancestor file.
  2. Restructure shared partials: extract common content into a leaf file both sides include instead.
  3. Grep included files for the offending file name to find all back-references.
  4. Ensure a self-referencing file no longer includes itself (file includes itself).

Example fix

<!-- before: a.mjml -->
<mj-include path="./b.mjml" />
<!-- b.mjml: <mj-include path="./a.mjml" />  CIRCULAR -->
<!-- after: b.mjml -->
<mj-text>content</mj-text>  <!-- remove include back to a.mjml -->
Defensive patterns

Strategy: validation

Validate before calling

const path = require('path');
function assertNoIncludeCycle(entry, resolve) {
  const stack = new Set();
  (function walk(f) {
    if (stack.has(f)) throw new Error(`Circular include: ${f}`);
    stack.add(f);
    for (const inc of includesOf(f)) walk(resolve(path.dirname(f), inc));
    stack.delete(f);
  })(entry);
}

Try / catch

try {
  return MJMLParser(xml, opts)
} catch (e) {
  if (e.message.startsWith('Circular inclusion detected')) {
    console.error('Include cycle at:', e.message)
  } else throw e
}

Prevention

When it happens

Trigger: Having a.mjml include b.mjml which includes a.mjml (direct cycle), or a longer cycle a->b->c->a, anywhere within <mj-include> tags parsed by MJMLParser (via parser -> handleInclude).

Common situations: Shared header/footer partials that include each other to pick up shared variables; refactor moving an include statement into a file that is itself included; accidental self-include of a file inside itself.

Related errors


AI-assisted analysis of mjmlio/mjml@6c01d35af5 (2026-09-02). Data as JSON: /api/errors/f9791210aa12b438. Report an issue: GitHub.