garrytan/gstack · error · Error

Cannot resolve real path: ${heatmapPath} (${err.code})

Error message

Cannot resolve real path: ${heatmapPath} (${err.code})

What it means

Heatmap-mode twin of error 180. The path validator for opts.heatmap output calls realpathSync on heatmapPath and throws this when the syscall fails with any code other than ENOENT. Protects the heatmap writer from unreadable/looping/non-directory segments before page.screenshot() writes the file.

Source

Thrown at browse/src/snapshot.ts:468

      const nodeFs = require('fs') as typeof import('fs');
      const absolute = nodePath.resolve(heatmapPath);
      const safeDirs = [TEMP_DIR, process.cwd()].map((d: string) => {
        try { return nodeFs.realpathSync(d); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; return d; }
      });
      let realPath: string;
      try {
        realPath = nodeFs.realpathSync(absolute);
      } catch (err: any) {
        if (err.code === 'ENOENT') {
          try {
            const dir = nodeFs.realpathSync(nodePath.dirname(absolute));
            realPath = nodePath.join(dir, nodePath.basename(absolute));
          } catch (err2: any) {
            if (err2?.code !== 'ENOENT') throw err2;
            realPath = absolute;
          }
        } else {
          throw new Error(`Cannot resolve real path: ${heatmapPath} (${err.code})`);
        }
      }
      if (!safeDirs.some((dir: string) => isPathWithin(realPath, dir))) {
        throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
      }
    }

    // Parse and validate color map
    const VALID_COLORS = new Set(['green', 'yellow', 'red', 'blue', 'orange', 'gray']);
    const COLOR_MAP: Record<string, { border: string; bg: string }> = {
      green:  { border: '#00b400', bg: 'rgba(0,180,0,0.15)' },
      yellow: { border: '#ffb400', bg: 'rgba(255,180,0,0.15)' },
      red:    { border: '#ff0000', bg: 'rgba(255,0,0,0.15)' },
      blue:   { border: '#0066ff', bg: 'rgba(0,102,255,0.15)' },
      orange: { border: '#ff6600', bg: 'rgba(255,102,0,0.15)' },
      gray:   { border: '#888888', bg: 'rgba(136,136,136,0.15)' },
    };

View on GitHub (pinned to 94993f7401)

Solutions

  1. Pass --output-path inside cwd or TEMP_DIR with a writable parent chain.
  2. Decode the errno in parentheses: EACCES → chmod/chown the parent; ELOOP → break the symlink cycle; ENOTDIR → ensure all prefix segments are directories.
  3. Omit --output-path to use `${TEMP_DIR}/browse-heatmap.png`.
  4. Run `namei -l <path>` to find the first segment denying traversal.

Example fix

// before
snapshot(page, { heatmap: '{"@e1":"red"}', outputPath: '/srv/http/hm.png' });
// after
snapshot(page, { heatmap: '{"@e1":"red"}', outputPath: './out/hm.png' });
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');
function assertHeatmapPathWritable(p: string): void {
  const dir = path.dirname(path.resolve(p));
  try { fs.accessSync(dir, fs.constants.W_OK | fs.constants.X_OK); }
  catch (e: any) { throw new Error(`Cannot resolve real path: ${p} (${e.code})`); }
}

Try / catch

try {
  await snapshot(session, { heatmap: json, outputPath });
} catch (e: any) {
  if (/^Cannot resolve real path:/.test(e.message)) {
    outputPath = path.join(require('os').tmpdir(), 'browse-heatmap.png');
    await snapshot(session, { heatmap: json, outputPath });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling snapshot with opts.heatmap set and an opts.outputPath (or its default) whose realpath cannot be resolved for a non-ENOENT reason: EACCES on a parent dir, ELOOP symlink cycle, ENOTDIR prefix.

Common situations: Same shape as 180 but on the heatmap branch: restrictive umask plus a /tmp mounted noexec; container where the temp dir is owned by root but the browser runs as nobody; a CI runner whose cwd is on a read-only mount.

Related errors


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