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
- Pre-warm mmdc once (run it on a trivial diagram) so puppeteer's chromium is already downloaded and chrome first-run setup is done.
- 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.
- For large/slow diagrams, render with render=native (the built-in subset) to avoid the external mmdc process entirely.
- Set OFFICECLI_MMDC to an explicit known-good mmdc path so the resolver isn't picking a broken install.
- 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
- Pre-warm mmdc once at startup so puppeteer's chromium download/first-run setup is done.
- For environments where chrome is unreliable, prefer render=native.
- Set OFFICECLI_MMDC to a known-good mmdc to avoid resolving a broken install.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- mmdc failed (exit {p.ExitCode}). {msg}
- failed to start mmdc.
- plugin_create_failed
- mermaid syntax error: {msg} (fix the mermaid source, or use
- headless browser produced no output.
AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13).
Data as JSON: /api/errors/2cd4fb23449364f9.
Report an issue: GitHub.