iOfficeAI/OfficeCLI · error · InvalidOperationException

mmdc timed out after 120s.

Error message

mmdc timed out after 120s.

What it means

The mermaid CLI (mmdc) was spawned to rasterize a diagram but did not exit within the hard 120-second ceiling imposed by Process.WaitForExit(120_000). On timeout the code kills the whole process tree (p.Kill(true)) and throws, so a single hung diagram costs two minutes. This is an infrastructure timeout, not a judgement of the diagram source.

Source

Thrown at src/officecli/Core/Diagram/MermaidImageRenderer.cs:328

            psi.ArgumentList.Add("-b"); psi.ArgumentList.Add(SafeBackground(background));
            psi.ArgumentList.Add("-s"); psi.ArgumentList.Add("2"); // HiDPI for crisp raster
            var pcfg = Environment.GetEnvironmentVariable("OFFICECLI_MMDC_PUPPETEER");
            if (!string.IsNullOrWhiteSpace(pcfg) && File.Exists(pcfg))
            {
                psi.ArgumentList.Add("-p"); psi.ArgumentList.Add(pcfg);
            }

            using var p = Process.Start(psi)
                ?? throw new InvalidOperationException("failed to start mmdc.");
            // Async-drain both streams: the serial stderr-then-stdout reads
            // interlocked when mmdc filled the stdout pipe first (bounded by
            // the 120s kill below, but a wasted two minutes per diagram).
            var errTask = p.StandardError.ReadToEndAsync();
            var outTask = p.StandardOutput.ReadToEndAsync();
            if (!p.WaitForExit(120_000))
            {
                try { p.Kill(true); } catch { /* best effort */ }
                throw new InvalidOperationException("mmdc timed out after 120s.");
            }
            if (p.ExitCode != 0 || !File.Exists(outPath))
            {
                var msg = $"{errTask.Result}{outTask.Result}".Trim();
                // A parse/unknown-type failure is bad input, not a broken mmdc; class
                // it as syntax so the Add path surfaces it (and does not fall back).
                if (LooksLikeSyntaxError(msg))
                    throw new MermaidSyntaxException(
                        $"mermaid syntax error: {msg} "
                        + "(fix the mermaid source, or use render=native for the built-in subset).");
                throw new InvalidOperationException($"mmdc failed (exit {p.ExitCode}). {msg}".Trim());
            }
            return outPath;
        }
        finally { try { File.Delete(inPath); } catch { /* best effort */ } }
    }

    // ----- chrome-family browser (mermaid.js in a page → sized screenshot) --------------

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Pre-warm mmdc once (run it on a trivial diagram) so puppeteer's chromium is already downloaded and chrome first-run setup is done.
  2. Verify chrome/puppeteer is healthy: run `mmdc -i test.mmd -o test.png` manually and time it; if it also hangs, fix the chrome install (`npx puppeteer browsers install chrome`) or missing shared libs.
  3. For large/slow diagrams, render with render=native (the built-in subset) to avoid the external mmdc process entirely.
  4. Set OFFICECLI_MMDC to an explicit known-good mmdc path so the resolver isn't picking a broken install.
  5. If transient (load/IO), catch InvalidOperationException around the render call and retry once.

Example fix

// before
var png = MermaidImageRenderer.Render(mermaid, bg);

// after — prefer native for large graphs, else guard the external call
if (UseNativeForLarge(mermaid)) {
    var png = MermaidImageRenderer.RenderNative(mermaid, bg);
} else {
    try { var png = MermaidImageRenderer.Render(mermaid, bg); }
    catch (InvalidOperationException ex) when (ex.Message.Contains("timed out")) {
        // log + fall back to native subset rather than failing the whole batch
        var png = MermaidImageRenderer.RenderNative(mermaid, bg);
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm mmdc is resolvable before relying on the external path
var mmdc = MermaidImageRenderer.ProbeMmdc();
if (string.IsNullOrEmpty(mmdc))
    throw new InvalidOperationException("mmdc not found; set OFFICECLI_MMDC or install @mermaid-js/mermaid-cli.");

Try / catch

try
{
    png = MermaidImageRenderer.Render(mermaid, bg);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("timed out", StringComparison.Ordinal))
{
    // transient under load — retry once, then fall back to the native subset
    try { png = MermaidImageRenderer.Render(mermaid, bg); }
    catch { png = MermaidImageRenderer.RenderNative(mermaid, bg); }
}

Prevention

When it happens

Trigger: RenderViaMmdc runs mmdc as an external process; p.WaitForExit(120_000) returns false because mmdc (puppeteer/chrome) is stuck — e.g. chrome waiting on a first-run download, an npm puppeteer install in progress, a very large graph layout, or the machine being CPU/IO saturated.

Common situations: First run on a machine where puppeteer's chromium hasn't been fetched yet; headless chrome blocked by missing system libs (libnss, libatk) so it hangs instead of erroring; CI runner under load; a flowchart with hundreds of nodes that elk/cytoscape layouts slowly.

Understand the failure class

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/2cd4fb23449364f9. Report an issue: GitHub.