garrytan/gstack · error · Error

2d canvas context unavailable

Error message

2d canvas context unavailable

What it means

Thrown by __rasterize() when canvas.getContext('2d') returns null after a successful SVG image decode. The browser refuses to hand out a 2D context when hardware-accelerated rendering is unavailable, the canvas has already been allocated a different context type, or the headless environment lacks GPU support. With no context the function cannot draw or call toDataURL, so it fails rather than returning a blank image.

Source

Thrown at lib/diagram-render/src/entry.ts:129

window.__rasterize = async (svgText: string, targetWidthPx: number): Promise<string> => {
  assertTargetWidth(targetWidthPx);
  const blob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  try {
    const img = new Image();
    await new Promise<void>((resolve, reject) => {
      img.onload = () => resolve();
      img.onerror = () => reject(new Error("SVG image decode failed (malformed SVG or foreignObject content)"));
      img.src = url;
    });
    const naturalW = img.naturalWidth || 800;
    const naturalH = img.naturalHeight || 600;
    const scale = targetWidthPx / naturalW;
    const canvas = document.createElement("canvas");
    canvas.width = Math.round(naturalW * scale);
    canvas.height = Math.round(naturalH * scale);
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("2d canvas context unavailable");
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    // Throws on tainted canvas — callers fall back to __mountForScreenshot +
    // `browse screenshot --selector "#raster-stage"`.
    return canvas.toDataURL("image/png");
  } finally {
    URL.revokeObjectURL(url);
  }
};

/**
 * Fallback rasterization stage: mount the SVG in the DOM so the caller can
 * take an element screenshot (no canvas, no taint rules). Returns a marker
 * string; the artifact is the screenshot, not the return value.
 */
window.__mountForScreenshot = (svgText: string, targetWidthPx: number): string => {
  document.getElementById("raster-stage")?.remove();

View on GitHub (pinned to 94993f7401)

Solutions

  1. Launch the browse daemon with software rendering enabled (e.g. add --use-gl=angle --use-angle=swiftshader or --enable-unsafe-swiftshader).
  2. Remove an unconditional --disable-gpu flag, or pair it with a software rasterizer backend.
  3. Fall back to __mountForScreenshot + `browse screenshot --selector "#raster-stage"` which needs no canvas.
  4. Verify the headless browser build bundles 2D canvas support.
Defensive patterns

Strategy: fallback

Try / catch

try {
  return await __rasterize(svgText, targetWidthPx);
} catch (e) {
  if (e instanceof Error && e.message === '2d canvas context unavailable') {
    // Fallback: mount the SVG and screenshot the element instead
    __mountForScreenshot(svgText, targetWidthPx);
    return browseScreenshot('#raster-stage');
  }
  throw e;
}

Prevention

When it happens

Trigger: document.createElement('canvas').getContext('2d') returns null inside the headless browser tab. Typical in headless Chromium with GPU disabled (--disable-gpu) and no software rasterizer, in a sandboxed CI environment, or after a prior call assigned a webgl context to the same canvas element (not the case here since each call creates a fresh canvas).

Common situations: CI runner with no GPU and --disable-gpu without SwiftShader; a constrained container that blocks GPU process access; an extremely memory-constrained tab that cannot allocate the backing store; rare browser build without 2d canvas support.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/485b07da1230ff3e. Report an issue: GitHub.